1
0
Fork 0
forked from wry/wry

Merge pull request #792 from mahkoh/jorth/egui

control-center: add in-process control center
This commit is contained in:
mahkoh 2026-03-13 19:25:15 +01:00 committed by GitHub
commit d29dea05fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
94 changed files with 9547 additions and 273 deletions

332
Cargo.lock generated
View file

@ -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"

View file

@ -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 = []

View file

@ -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<String> {
let dir = WalkDir::new(tree.root);

View file

@ -1035,10 +1035,21 @@ impl ConfigClient {
position
}
pub fn set_egui_fonts(&self, proportional: Option<Vec<&str>>, monospace: Option<Vec<&str>>) {
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 });
}

View file

@ -841,6 +841,11 @@ pub enum ClientMessage<'a> {
fds: Vec<(i32, i32)>,
tag: Option<&'a str>,
},
SetEguiFonts {
proportional: Option<Vec<&'a str>>,
monospace: Option<Vec<&'a str>>,
},
OpenControlCenter,
}
#[derive(Serialize, Deserialize, Debug)]

View file

@ -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();
}

View file

@ -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<Item = &'a str>) {
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<Item = &'a str>) {
get!().set_egui_fonts(None, Some(fonts.into_iter().collect()));
}
/// Elements of the compositor whose color can be changed.
pub mod colors {
use {

View file

@ -588,7 +588,6 @@ pub trait BackendDrmDevice {
fn set_flip_margin(&self, margin: u64) {
let _ = margin;
}
#[expect(dead_code)]
fn flip_margin(&self) -> Option<u64> {
None
}

View file

@ -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),
}
}

32
src/cli/control_center.rs Normal file
View file

@ -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<ToolClient>,
}
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;
}
}

View file

@ -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<State>) -> Vec<SpawnedFuture<()>> {
"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<State>) {
wlr_output_heads: Default::default(),
});
let schedule = Rc::new(OutputSchedule::new(
&state.ring,
&state.eng,
state,
&connector_data,
&persistent_state,
));

View file

@ -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<Vec<&str>>, monospace: Option<Vec<&str>>) {
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(())
}

681
src/control_center.rs Normal file
View file

@ -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<State>) {
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<ControlCenterInterest>,
redraw: AsyncEvent,
control_centers: CopyHashMap<ControlCenterId, Rc<ControlCenter>>,
}
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<ControlCenterInner>,
}
linear_ids!(PaneIds, PaneId, u64);
struct ControlCenterInner {
id: ControlCenterId,
state: Rc<State>,
tree: RefCell<Settings>,
window: Rc<EggWindow>,
pane_ids: PaneIds,
interests: Rc<Interests>,
}
#[derive(Default)]
struct Interests {
interests: NumCell<ControlCenterInterest>,
interests_array: [NumCell<u64>; <ControlCenterInterest as Bitflag>::Type::BITS as usize],
}
struct Settings {
tree: Tree<Pane>,
}
struct Pane {
id: PaneId,
ps: PaneState,
own_interests: ControlCenterInterest,
cc_interests: Rc<Interests>,
ty: PaneType,
}
struct PaneState {
errors: Vec<String>,
}
enum PaneType {
Compositor(CompositorPane),
Idle(IdlePane),
ColorManagement(ColorManagementPane),
Xwayland(XwaylandPane),
Outputs(Box<OutputsPane>),
GPUs(GpusPane),
Input(InputPane),
LookAndFeel(LookAndFeelPane),
Clients(ClientsPane),
Client(ClientPane),
WindowSearch(WindowSearchPane),
Window(WindowPane),
}
struct CcBehavior<'a> {
cc: &'a Rc<ControlCenterInner>,
close: Option<TileId>,
open: Option<PaneType>,
}
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<Pane> 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<Pane>, tile_id: TileId) -> WidgetText {
fn add_title(tiles: &Tiles<Pane>, 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<Pane>,
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<Pane>,
_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<Self>, 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<Self>) -> Result<Rc<ControlCenter>, 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<Pane>, 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<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
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<R>(
ui: &mut Ui,
id_salt: impl Hash,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
let mut spacing = ui.spacing().item_spacing;
spacing.x *= 3.0;
Grid::new(id_salt).spacing(spacing).show(ui, add_contents)
}
fn row<R>(ui: &mut Ui, name: &str, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
row_ui(ui, name, |_| (), add_contents)
}
fn row_ui<R, S>(
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<R>(
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<R>(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<T>(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<R, T>(
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<N>(
ui: &mut Ui,
name: &str,
old: N,
range: RangeInclusive<N>,
speed: f64,
set: impl FnOnce(N),
) where
N: Numeric,
{
drag_value_ui(ui, name, |_| (), old, range, speed, set);
}
fn drag_value_ui<R, N>(
ui: &mut Ui,
name: &str,
label: impl FnOnce(&mut Ui) -> R,
mut v: N,
range: RangeInclusive<N>,
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<WidgetText>) {
row(ui, name, |ui| ui.label(text));
}
trait GridExt {
fn row(&mut self) -> impl DerefMut<Target = Ui>;
}
impl GridExt for Ui {
fn row(&mut self) -> impl DerefMut<Target = Ui> {
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>) {
self.window.request_redraw();
}
}

View file

@ -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<Client>;
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<State>) -> Option<Rc<dyn CritUpstreamNode<Self::Target>>> {
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<dyn CritUpstreamNode<Self::Target>>,
) -> Rc<dyn CritUpstreamNode<Self::Target>> {
state.cl_matcher_manager.not(upstream)
}
fn list(
state: &State,
upstream: &[Rc<dyn CritUpstreamNode<Self::Target>>],
all: bool,
) -> Rc<dyn CritUpstreamNode<Self::Target>> {
state.cl_matcher_manager.list(upstream, all)
}
fn exactly(
state: &State,
n: usize,
upstream: &[Rc<dyn CritUpstreamNode<Self::Target>>],
) -> Rc<dyn CritUpstreamNode<Self::Target>> {
state.cl_matcher_manager.exactly(upstream, n)
}
}
pub struct ClientsPane {
state: Rc<State>,
filter: bool,
criterion: CcCriterion<ClientCrit>,
matched: Rc<Matched>,
leaf: Option<Rc<CritLeafMatcher<Rc<Client>>>>,
}
struct Matched {
slf: Weak<ControlCenterInner>,
clients: CopyHashMap<ClientId, ()>,
}
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<Self>) -> 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<Client>,
}
impl ControlCenterInner {
pub fn create_client_pane(self: &Rc<Self>, client: &Rc<Client>) -> 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<Client>) {
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::<ClientWindowMatchersCache>()
.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<ClientId, CachedWindowMatcher>,
}
struct CachedWindowMatcher {
generation: u64,
_matcher: Rc<CritLeafMatcher<ToplevelData>>,
matchers: Rc<WindowMatchers>,
}
struct WindowMatchers {
cc: Weak<ControlCenterInner>,
windows: CopyHashMap<ToplevelIdentifier, ()>,
}
impl ClientWindowMatchersCache {
fn get(&mut self, cc: &Rc<ControlCenterInner>, id: ClientId) -> &Rc<WindowMatchers> {
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
}
}

View file

@ -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<State>,
}
impl ControlCenterInner {
pub fn create_color_management_pane(self: &Rc<Self>) -> 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());
});
}
}

View file

@ -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<State>,
switch_to_vt: u32,
}
impl ControlCenterInner {
pub fn create_compositor_pane(self: &Rc<Self>) -> 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);
}
});
}
}

View file

@ -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<T> {
Not(Box<Self>),
List(Vec<Self>, bool),
Exactly(usize, Vec<Self>),
T(T),
}
impl<T> Default for CcCriterion<T>
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<T> {
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<T> StaticText for CritTy<T>
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<State>) -> Option<Rc<dyn CritUpstreamNode<Self::Target>>>;
fn not(
state: &State,
upstream: &Rc<dyn CritUpstreamNode<Self::Target>>,
) -> Rc<dyn CritUpstreamNode<Self::Target>>;
fn list(
state: &State,
upstream: &[Rc<dyn CritUpstreamNode<Self::Target>>],
all: bool,
) -> Rc<dyn CritUpstreamNode<Self::Target>>;
fn exactly(
state: &State,
n: usize,
upstream: &[Rc<dyn CritUpstreamNode<Self::Target>>],
) -> Rc<dyn CritUpstreamNode<Self::Target>>;
}
impl<T> CcCriterion<T>
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<T::Type> {
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<State>) -> Option<Rc<dyn CritUpstreamNode<T::Target>>> {
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<Option<Regex>>,
}
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<CritLiteralOrRegex> {
match &self.regex {
None => Some(CritLiteralOrRegex::Literal(self.text.clone())),
Some(v) => Some(CritLiteralOrRegex::Regex(v.clone()?)),
}
}
}

View file

@ -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<State>,
}
impl ControlCenterInner {
pub fn create_gpus_pane(self: &Rc<Self>) -> 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::<Vec<_>>();
cs.sort();
for c in cs {
ui.label(c);
}
});
});
}
}

View file

@ -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<State>,
}
impl ControlCenterInner {
pub fn create_idle_pane(self: &Rc<Self>) -> 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);
});
}
});
}
}

View file

@ -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<State>,
paste_requested: Option<Id>,
keymaps: AHashMap<Key, KeymapState>,
}
#[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<Rc<KbvmMap>>,
pointer_revert_key: Keysym,
pointer_revert_key_str: Option<String>,
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<Self>) -> 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<WlSeatGlobal>) {
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<DeviceHandlerData>,
seats: &[&Rc<WlSeatGlobal>],
outputs: &[&Rc<WlOutputGlobal>],
) {
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<KbvmMap>>) {
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<Id>,
ks: &mut KeymapState,
ui: &mut Ui,
map: Option<&Rc<KbvmMap>>,
set_map: impl Fn(&Rc<KbvmMap>),
) {
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<T, const W: usize>(
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<R, T, const W: usize>(
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);
}
}
}

View file

@ -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<State>,
}
impl ControlCenterInner {
pub fn create_look_and_feel_pane(self: &Rc<Self>) -> 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);
}
}
});
});
}
}

File diff suppressed because it is too large Load diff

View file

@ -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<Vec<PaneName>> = 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<Self>, tree: &mut Tree<Pane>, 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);
})
},
);
}
}

View file

@ -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<ClientCrit>),
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<State>) -> Option<Rc<dyn CritUpstreamNode<Self::Target>>> {
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<dyn CritUpstreamNode<Self::Target>>,
) -> Rc<dyn CritUpstreamNode<Self::Target>> {
state.tl_matcher_manager.not(upstream)
}
fn list(
state: &State,
upstream: &[Rc<dyn CritUpstreamNode<Self::Target>>],
all: bool,
) -> Rc<dyn CritUpstreamNode<Self::Target>> {
state.tl_matcher_manager.list(upstream, all)
}
fn exactly(
state: &State,
n: usize,
upstream: &[Rc<dyn CritUpstreamNode<Self::Target>>],
) -> Rc<dyn CritUpstreamNode<Self::Target>> {
state.tl_matcher_manager.exactly(upstream, n)
}
}
pub struct WindowSearchPane {
state: Rc<State>,
criterion: CcCriterion<WindowClit>,
matched: Rc<Matched>,
leaf: Option<Rc<CritLeafMatcher<ToplevelData>>>,
}
struct Matched {
slf: Weak<ControlCenterInner>,
windows: CopyHashMap<ToplevelIdentifier, ()>,
}
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<Self>) -> 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<dyn ToplevelNode>,
}
impl ControlCenterInner {
pub fn create_window_pane(self: &Rc<Self>, window: &Rc<dyn ToplevelNode>) -> 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<dyn ToplevelNode>,
) {
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::<WindowPropertyListeners>()
.ensure(behavior.cc, data);
});
}
#[derive(Default)]
struct WindowPropertyListeners {
generation: u64,
listeners: AHashMap<NodeId, WindowPropertyListener>,
}
struct WindowPropertyListener {
_listener: EventListener<dyn LazyEventSourceListener>,
generation: u64,
}
impl WindowPropertyListeners {
fn ensure(&mut self, cc: &Rc<ControlCenterInner>, data: &ToplevelData) {
let listener = self.listeners.entry(data.node_id).or_insert_with(|| {
let listener =
EventListener::new(Rc::downgrade(cc) as Weak<dyn LazyEventSourceListener>);
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
}

View file

@ -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<State>,
}
impl ControlCenterInner {
pub fn create_xwayland_pane(self: &Rc<Self>) -> 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);
}
}
}

View file

@ -234,7 +234,6 @@ impl ClMatcherManager {
self.root(ClmMatchTag::new(string))
}
#[expect(dead_code)]
pub fn id(&self, id: ClientId) -> Rc<ClmUpstreamNode> {
self.root(ClmMatchId(id))
}

View file

@ -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()
}

3
src/egui_adapter.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod egui_oklch;
pub mod egui_platform;
mod egui_vulkan;

View file

@ -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<Color32> for Oklch {
fn into(self) -> Color32 {
self.to_oklab().into()
}
}
impl Into<Color32> 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()
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

BIN
src/egui_adapter/icons.ttf Normal file

Binary file not shown.

View file

@ -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;
}

View file

@ -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);
}

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,2 @@
7eb8fae39ae513bc4f6973c12227aa4aa43734bdf34c90e1b3b69294ad98db87 src/egui_adapter/shaders/shader.frag
501f4d0c5c5f10a371659b89f12d87abb03e5b57a31dbae5f3c6ca5726e4db01 src/egui_adapter/shaders/shader.vert

View file

@ -34,14 +34,12 @@ pub enum FontconfigError {
}
#[derive(Debug)]
#[expect(dead_code)]
pub struct Font {
pub fullname: String,
pub file: PathBuf,
pub index: Option<i32>,
}
#[expect(dead_code)]
pub fn match_font(family: &str) -> Result<Font, FontconfigError> {
thread_local! {
static CONFIG: *mut FcConfig = FcConfigGetCurrent();

View file

@ -256,7 +256,7 @@ pub struct Globals {
removed: CopyHashMap<GlobalName, Rc<dyn Global>>,
pub outputs: CopyHashMap<GlobalName, Rc<WlOutputGlobal>>,
pub seats: CopyHashMap<GlobalName, Rc<WlSeatGlobal>>,
singletons: StaticMap<Singleton, GlobalName>,
pub singletons: StaticMap<Singleton, GlobalName>,
exposed: StaticMap<Singleton, Cell<bool>>,
}

View file

@ -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;

View file

@ -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<GlobalName>) {
pub fn update_in_compositor_space(&mut self, wl_output: Option<GlobalName>) {
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<RefCell<HeadState>>,
managers: CopyHashMap<(ClientId, JayHeadManagerSessionV1Id), Rc<Head>>,
}
@ -235,7 +234,6 @@ impl HeadManagers {
}
}
#[expect(dead_code)]
pub fn state(&self) -> ReadOnlyHeadState {
ReadOnlyHeadState {
state: self.state.clone(),

View file

@ -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<Self>,
) -> 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! {

View file

@ -68,14 +68,14 @@ impl JayIdleRequestHandler for JayIdle {
fn set_interval(&self, req: SetInterval, _slf: &Rc<Self>) -> 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<Self>) -> 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(())
}
}

View file

@ -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<Client>,
pub state: Rc<State>,
pub tracker: Tracker<Self>,
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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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(())
},
)

View file

@ -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<Client>,
pub tracker: Tracker<Self>,
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<Self>) -> 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<ClientError>),
}
efrom!(JayOpenControlCenterRequestError, ClientError);

View file

@ -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(())
}

View file

@ -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<ConfigFallbackOutputMode> 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<Self>) {
@ -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<dyn Node>) -> SmallVec<[Rc<WlSeatGlobal>; 3]> {
}
impl DeviceHandlerData {
pub fn set_seat(&self, seat: Option<Rc<WlSeatGlobal>>) {
pub fn set_seat(&self, state: &State, seat: Option<Rc<WlSeatGlobal>>) {
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<Rc<KbvmMap>>) {
pub fn set_keymap(&self, state: &State, keymap: Option<Rc<KbvmMap>>) {
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);
}
}

View file

@ -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 {

View file

@ -44,12 +44,12 @@ impl ZwpIdleInhibitorV1 {
pub fn activate(self: &Rc<Self>) {
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);
}
}

View file

@ -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,

View file

@ -80,7 +80,6 @@ impl Logger {
log::set_max_level(filter);
}
#[expect(dead_code)]
pub fn level(&self) -> LogLevel {
self.level.load(Relaxed)
}

View file

@ -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;

View file

@ -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<IoUring>,
eng: &Rc<AsyncEngine>,
state: &State,
connector: &Rc<ConnectorData>,
persistent: &Rc<PersistentOutputState>,
) -> 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();
}

View file

@ -42,6 +42,15 @@ pub struct AcceptorMetadata {
pub tag: Option<String>,
}
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() {

View file

@ -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<ActivationToken, ()>,
pub toplevel_lists:
CopyHashMap<(ClientId, ExtForeignToplevelListV1Id), Rc<ExtForeignToplevelListV1>>,
pub dma_buf_ids: DmaBufIds,
pub dma_buf_ids: Rc<DmaBufIds>,
pub drm_feedback_ids: DrmFeedbackIds,
pub direct_scanout_enabled: Cell<bool>,
pub persistent_output_states: CopyHashMap<Rc<OutputId>, Rc<PersistentOutputState>>,
@ -292,6 +299,9 @@ pub struct State {
pub supports_presentation_feedback: Cell<bool>,
pub eventfd_cache: Rc<EventfdCache>,
pub lazy_event_sources: Rc<LazyEventSources>,
pub bo_drop_queue: Rc<ObjectDropQueue<Rc<dyn BufferObject>>>,
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<ZwpIdleInhibitorV1>) {
pub fn add_inhibitor(&self, state: &State, inhibitor: &Rc<ZwpIdleInhibitorV1>) {
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<Rc<dyn GfxContext>>) {
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<Self>, 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<ContainerNode>) {
@ -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<Vec<&str>>, monospace: Option<Vec<&str>>) {
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();

View file

@ -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<State>, connector: &Rc<dyn Connector>) {
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() {

View file

@ -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);
}
}

View file

@ -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));

View file

@ -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<Self>, 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);
}
}

View file

@ -936,7 +936,6 @@ impl ToplevelData {
parent.node_is_workspace()
}
#[expect(dead_code)]
pub fn property_changed_source(&self) -> &Rc<LazyEventSource> {
self.property_changed_source
.get_or_init(|| self.state.lazy_event_sources.create_source())

View file

@ -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;

View file

@ -100,6 +100,14 @@ impl<T> NumCell<T> {
{
!self.is_zero()
}
#[inline(always)]
pub fn take(&self) -> T
where
T: Default,
{
self.t.replace(T::default())
}
}
impl<T: BitOr<Output = T> + Copy> BitOr<T> for &'_ NumCell<T> {

View file

@ -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<T> {
ring: Rc<IoUring>,
killed: Cell<bool>,
pending: RefCell<Vec<Option<(T, PendingPoll)>>>,
stack: Stack<Rc<Pollable<T>>>,
}
struct Pollable<T> {
queue: Rc<ObjectDropQueue<T>>,
idx: usize,
}
impl<T> ObjectDropQueue<T> {
pub fn new(ring: &Rc<IoUring>) -> Self {
Self {
ring: ring.clone(),
killed: Default::default(),
pending: Default::default(),
stack: Default::default(),
}
}
pub fn push(self: &Rc<Self>, fd: &Rc<OwnedFd>, 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<T> PollCallback for Pollable<T> {
fn completed(self: Rc<Self>, res: Result<c_short, OsError>) {
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());
}
}
}

View file

@ -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,

View file

@ -14,7 +14,6 @@ pub fn pipe() -> Result<Pipe<OwnedFd, OwnedFd>, OsError> {
}
impl<L, R> Pipe<L, R> {
#[expect(dead_code)]
pub fn map_read<Lprime>(self, map: impl FnOnce(L) -> Lprime) -> Pipe<Lprime, R> {
Pipe {
read: map(self.read),
@ -22,7 +21,6 @@ impl<L, R> Pipe<L, R> {
}
}
#[expect(dead_code)]
pub fn map_write<Rprime>(self, map: impl FnOnce(R) -> Rprime) -> Pipe<L, Rprime> {
Pipe {
read: self.read,

View file

@ -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 {}

View file

@ -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<Option<SyncFile>, 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 _;

View file

@ -189,7 +189,6 @@ impl UsrJayCompositor {
obj
}
#[expect(dead_code)]
pub fn get_sync_file_surface(&self, surface: &UsrWlSurface) -> Rc<UsrJaySyncFileSurface> {
let obj = Rc::new(UsrJaySyncFileSurface {
id: self.con.id(),

View file

@ -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<UsrJaySyncFileRelease> {
let obj = Rc::new(UsrJaySyncFileRelease {
id: self.con.id(),

View file

@ -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,

View file

@ -21,7 +21,6 @@ pub struct UsrWlDataDeviceManager {
}
impl UsrWlDataDeviceManager {
#[expect(dead_code)]
pub fn create_data_source(&self) -> Rc<UsrWlDataSource> {
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<UsrWlDataDevice> {
let obj = Rc::new(UsrWlDataDevice {
id: self.con.id(),

View file

@ -17,7 +17,6 @@ pub struct UsrWlDataOffer {
}
impl UsrWlDataOffer {
#[expect(dead_code)]
pub fn receive(&self, mime_type: &str, fd: &Rc<OwnedFd>) {
self.con.request(Receive {
self_id: self.id,

View file

@ -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,

View file

@ -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,

View file

@ -47,7 +47,6 @@ impl UsrWlSeat {
ptr
}
#[expect(dead_code)]
pub fn get_keyboard(&self) -> Rc<UsrWlKeyboard> {
let kb = Rc::new(UsrWlKeyboard {
id: self.con.id(),

View file

@ -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,

View file

@ -21,7 +21,6 @@ pub struct UsrWpCursorShapeManagerV1 {
}
impl UsrWpCursorShapeManagerV1 {
#[expect(dead_code)]
pub fn get_pointer(&self, pointer: &UsrWlPointer) -> Rc<UsrWpCursorShapeDeviceV1> {
let obj = Rc::new(UsrWpCursorShapeDeviceV1 {
id: self.con.id(),

View file

@ -22,7 +22,6 @@ pub trait UsrXdgSurfaceOwner {
}
impl UsrXdgSurface {
#[expect(dead_code)]
pub fn get_toplevel(&self) -> Rc<UsrXdgToplevel> {
let obj = Rc::new(UsrXdgToplevel {
id: self.con.id(),

View file

@ -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 => {

View file

@ -18,7 +18,6 @@ pub struct UsrXdgWmBase {
}
impl UsrXdgWmBase {
#[expect(dead_code)]
pub fn get_xdg_surface(&self, surface: &UsrWlSurface) -> Rc<UsrXdgSurface> {
let obj = Rc::new(UsrXdgSurface {
id: self.con.id(),

View file

@ -22,7 +22,6 @@ pub struct UsrZwpLinuxDmabufV1 {
}
impl UsrZwpLinuxDmabufV1 {
#[expect(dead_code)]
pub fn create_buffer(&self, buffer: &DmaBuf) -> Rc<UsrWlBuffer> {
let params = Rc::new(UsrZwpLinuxBufferParamsV1 {
id: self.con.id(),

View file

@ -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<State>) {
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());

View file

@ -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<i32>,
}
#[derive(Debug, Clone, Default)]
pub struct Egui {
pub proportional_fonts: Option<Vec<String>>,
pub monospace_fonts: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct Status {
pub format: MessageFormat,
@ -510,6 +517,7 @@ pub struct Config {
pub auto_reload: Option<bool>,
pub log_level: Option<LogLevel>,
pub theme: Theme,
pub egui: Egui,
pub gfx_api: Option<GfxApi>,
pub direct_scanout_enabled: Option<bool>,
pub drm_devices: Vec<ConfigDrmDevice>,

View file

@ -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;

View file

@ -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)

View file

@ -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(),

View file

@ -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<String>, Spanned<Value>>,
) -> ParseResult<Self> {
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,
})
}
}

View file

@ -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" }

View file

@ -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<Persistent
if let Some(v) = config.fallback_output_mode {
persistent.seat.set_fallback_output_mode(v);
}
if let Some(f) = &config.egui.proportional_fonts {
set_egui_proportional_fonts(f.iter().map(|s| &**s));
}
if let Some(f) = &config.egui.monospace_fonts {
set_egui_monospace_fonts(f.iter().map(|s| &**s));
}
}
fn create_command(exec: &Exec) -> Command {

View file

@ -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": {

View file

@ -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).
<a name="types-Connector"></a>
### `Connector`
@ -2588,6 +2594,32 @@ The table has the following fields:
The numbers should be integers.
<a name="types-Egui"></a>
### `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.
<a name="types-Eotf"></a>
### `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.
<a name="types-SimpleIm"></a>

View file

@ -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"]`.

View file

@ -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 {

View file

@ -0,0 +1,7 @@
request destroy {
}
event failed {
msg: str,
}