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. # It is not intended for manual editing.
version = 4 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]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.25.1" version = "0.25.1"
@ -100,9 +116,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.101" version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "arrayref" name = "arrayref"
@ -148,9 +164,9 @@ dependencies = [
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.10.0" version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]] [[package]]
name = "blake3" name = "blake3"
@ -178,9 +194,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.19.1" version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
@ -196,9 +212,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.55" version = "1.2.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@ -223,9 +239,9 @@ dependencies = [
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.43" version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
@ -236,9 +252,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.57" version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -246,9 +262,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.57" version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -259,9 +275,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_complete" name = "clap_complete"
version = "4.5.65" version = "4.5.66"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031"
dependencies = [ dependencies = [
"clap", "clap",
] ]
@ -275,14 +291,14 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.114", "syn 2.0.117",
] ]
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.7.7" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
@ -337,9 +353,9 @@ checksum = "24efe21bd9a78102d1225f10f0a41d9d5b43f4df7ae8235f39a9c79e4d476c1e"
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.5.5" version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [ dependencies = [
"powerfmt", "powerfmt",
] ]
@ -365,6 +381,72 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@ -426,38 +508,38 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.31" version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.31" version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.31" version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.114", "syn 2.0.117",
] ]
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.31" version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.31" version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-io", "futures-io",
@ -465,7 +547,6 @@ dependencies = [
"futures-task", "futures-task",
"memchr", "memchr",
"pin-project-lite", "pin-project-lite",
"pin-utils",
"slab", "slab",
] ]
@ -488,19 +569,19 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"r-efi", "r-efi 5.3.0",
"wasip2", "wasip2",
] ]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.4.1" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"r-efi", "r-efi 6.0.0",
"rand_core 0.10.0", "rand_core 0.10.0",
"wasip2", "wasip2",
"wasip3", "wasip3",
@ -618,6 +699,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1697e6b71679da96d5c41bb9035116141baadbf59a60625fd66cb3c9584e7b0" checksum = "f1697e6b71679da96d5c41bb9035116141baadbf59a60625fd66cb3c9584e7b0"
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.17" version = "1.0.17"
@ -658,6 +748,8 @@ dependencies = [
"clap", "clap",
"clap_complete", "clap_complete",
"dirs", "dirs",
"egui",
"egui_tiles",
"futures-util", "futures-util",
"gpu-alloc", "gpu-alloc",
"gpu-alloc-types", "gpu-alloc-types",
@ -734,9 +826,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.85" version = "0.3.91"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
@ -773,7 +865,7 @@ checksum = "28067e7361c0069c3753795d131653f9ea5333aeb35a3855fb2de66447c48ac8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.114", "syn 2.0.117",
] ]
[[package]] [[package]]
@ -790,9 +882,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.181" version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]] [[package]]
name = "libloading" name = "libloading"
@ -806,19 +898,18 @@ dependencies = [
[[package]] [[package]]
name = "libredox" name = "libredox"
version = "0.1.12" version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
dependencies = [ dependencies = [
"bitflags",
"libc", "libc",
] ]
[[package]] [[package]]
name = "linearize" name = "linearize"
version = "0.1.5" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d5b35550da9461fb8d3acf71c9925afae570bfc45a11857b55138d25c8604d" checksum = "f6e1430c89633736996fd763822abd252e395dbccaaee33be601b4d59678a93e"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"linearize-derive", "linearize-derive",
@ -835,14 +926,14 @@ checksum = "f657db73fbcad5341c5991ddee6c464d4bfd521575c0dc1a47913e0f434defeb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.114", "syn 2.0.117",
] ]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.11.0" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@ -875,6 +966,12 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "nohash-hasher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.0" version = "0.2.0"
@ -889,7 +986,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.114", "syn 2.0.117",
] ]
[[package]] [[package]]
@ -943,6 +1040,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 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]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.5" version = "0.12.5"
@ -997,7 +1103,7 @@ dependencies = [
"phf_shared", "phf_shared",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.114", "syn 2.0.117",
] ]
[[package]] [[package]]
@ -1011,35 +1117,29 @@ dependencies = [
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.10" version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
dependencies = [ dependencies = [
"pin-project-internal", "pin-project-internal",
] ]
[[package]] [[package]]
name = "pin-project-internal" name = "pin-project-internal"
version = "1.1.10" version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.114", "syn 2.0.117",
] ]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
@ -1049,9 +1149,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]] [[package]]
name = "png" name = "png"
version = "0.18.0" version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"crc32fast", "crc32fast",
@ -1073,7 +1173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"syn 2.0.114", "syn 2.0.117",
] ]
[[package]] [[package]]
@ -1086,19 +1186,25 @@ dependencies = [
] ]
[[package]] [[package]]
name = "quick-xml" name = "profiling"
version = "0.39.0" version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"memchr", "memchr",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.44" version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -1109,6 +1215,12 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -1134,7 +1246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
dependencies = [ dependencies = [
"chacha20", "chacha20",
"getrandom 0.4.1", "getrandom 0.4.2",
"rand_core 0.10.0", "rand_core 0.10.0",
] ]
@ -1201,9 +1313,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.9" version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "repc" name = "repc"
@ -1234,9 +1346,9 @@ checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "1.1.3" version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
@ -1314,7 +1426,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.114", "syn 2.0.117",
] ]
[[package]] [[package]]
@ -1410,9 +1522,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.114" version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1455,7 +1567,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.114", "syn 2.0.117",
] ]
[[package]] [[package]]
@ -1538,6 +1650,12 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "uapi" name = "uapi"
version = "0.2.13" version = "0.2.13"
@ -1566,9 +1684,15 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.23" version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "unicode-width" name = "unicode-width"
@ -1636,9 +1760,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.108" version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@ -1649,9 +1773,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.108" version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -1659,22 +1783,22 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.108" version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.114", "syn 2.0.117",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.108" version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -1743,7 +1867,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.114", "syn 2.0.117",
] ]
[[package]] [[package]]
@ -1754,7 +1878,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.114", "syn 2.0.117",
] ]
[[package]] [[package]]
@ -1967,7 +2091,7 @@ dependencies = [
"heck", "heck",
"indexmap", "indexmap",
"prettyplease", "prettyplease",
"syn 2.0.114", "syn 2.0.117",
"wasm-metadata", "wasm-metadata",
"wit-bindgen-core", "wit-bindgen-core",
"wit-component", "wit-component",
@ -1983,7 +2107,7 @@ dependencies = [
"prettyplease", "prettyplease",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.114", "syn 2.0.117",
"wit-bindgen-core", "wit-bindgen-core",
"wit-bindgen-rust", "wit-bindgen-rust",
] ]
@ -2055,26 +2179,26 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.39" version = "0.8.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.39" version = "0.8.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.114", "syn 2.0.117",
] ]
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.20" version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" 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" with_builtin_macros = "0.1.0"
blake3 = "1.8.2" blake3 = "1.8.2"
run-on-drop = "1.0.0" 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] [build-dependencies]
repc = "0.1.1" repc = "0.1.1"
@ -87,6 +89,38 @@ opt-level = 3
[profile.dev.package."smallvec"] [profile.dev.package."smallvec"]
opt-level = 3 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] [features]
rc_tracking = [] rc_tracking = []
it = [] it = []

View file

@ -8,23 +8,31 @@ pub struct Tree {
pub shaders: &'static [&'static str], pub shaders: &'static [&'static str],
} }
pub const TREES: &[Tree] = &[Tree { pub const TREES: &[Tree] = &[
root: "src/gfx_apis/vulkan/shaders", Tree {
hash: "src/gfx_apis/vulkan/shaders_hash.txt", root: "src/gfx_apis/vulkan/shaders",
bin: "src/gfx_apis/vulkan/shaders_bin", hash: "src/gfx_apis/vulkan/shaders_hash.txt",
shaders: &[ bin: "src/gfx_apis/vulkan/shaders_bin",
"fill.frag", shaders: &[
"fill.vert", "fill.frag",
"tex.vert", "fill.vert",
"tex.frag", "tex.vert",
"out.vert", "tex.frag",
"out.frag", "out.vert",
"legacy/fill.frag", "out.frag",
"legacy/fill.vert", "legacy/fill.frag",
"legacy/tex.vert", "legacy/fill.vert",
"legacy/tex.frag", "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> { fn calculate_hash(tree: &Tree) -> anyhow::Result<String> {
let dir = WalkDir::new(tree.root); let dir = WalkDir::new(tree.root);

View file

@ -1035,10 +1035,21 @@ impl ConfigClient {
position 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) { pub fn set_middle_click_paste_enabled(&self, enabled: bool) {
self.send(&ClientMessage::SetMiddleClickPasteEnabled { enabled }); self.send(&ClientMessage::SetMiddleClickPasteEnabled { enabled });
} }
pub fn open_control_center(&self) {
self.send(&ClientMessage::OpenControlCenter);
}
pub fn set_workspace_display_order(&self, order: WorkspaceDisplayOrder) { pub fn set_workspace_display_order(&self, order: WorkspaceDisplayOrder) {
self.send(&ClientMessage::SetWorkspaceDisplayOrder { order }); self.send(&ClientMessage::SetWorkspaceDisplayOrder { order });
} }

View file

@ -841,6 +841,11 @@ pub enum ClientMessage<'a> {
fds: Vec<(i32, i32)>, fds: Vec<(i32, i32)>,
tag: Option<&'a str>, tag: Option<&'a str>,
}, },
SetEguiFonts {
proportional: Option<Vec<&'a str>>,
monospace: Option<Vec<&'a str>>,
},
OpenControlCenter,
} }
#[derive(Serialize, Deserialize, Debug)] #[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) { pub fn set_middle_click_paste_enabled(enabled: bool) {
get!().set_middle_click_paste_enabled(enabled); 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() 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. /// Elements of the compositor whose color can be changed.
pub mod colors { pub mod colors {
use { use {

View file

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

View file

@ -1,6 +1,7 @@
mod clients; mod clients;
mod color; mod color;
mod color_management; mod color_management;
mod control_center;
mod damage_tracking; mod damage_tracking;
mod duration; mod duration;
mod generate; mod generate;
@ -97,6 +98,8 @@ pub enum Cmd {
Clients(ClientsArgs), Clients(ClientsArgs),
/// Inspect the surface tree. /// Inspect the surface tree.
Tree(TreeArgs), Tree(TreeArgs),
/// Opens the control center.
ControlCenter,
/// Prints the Jay version and exits. /// Prints the Jay version and exits.
Version, Version,
/// Prints the Jay PID and exits. /// Prints the Jay PID and exits.
@ -243,5 +246,6 @@ pub fn main() {
#[cfg(feature = "it")] #[cfg(feature = "it")]
Cmd::RunTests => crate::it::run_tests(), Cmd::RunTests => crate::it::run_tests(),
Cmd::Reexec(a) => reexec::main(cli.global, a), 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}, clientmem::{self, ClientMemError},
cmm::{cmm_manager::ColorManager, cmm_primaries::Primaries}, cmm::{cmm_manager::ColorManager, cmm_primaries::Primaries},
config::ConfigProxy, config::ConfigProxy,
control_center::redraw_control_centers,
copy_device::CopyDeviceRegistry, copy_device::CopyDeviceRegistry,
cpu_worker::{CpuWorker, CpuWorkerError}, cpu_worker::{CpuWorker, CpuWorkerError},
criteria::{ criteria::{
@ -72,6 +73,7 @@ use {
fdcloser::FdCloser, fdcloser::FdCloser,
nice::{did_elevate_scheduler, elevate_scheduler}, nice::{did_elevate_scheduler, elevate_scheduler},
numcell::NumCell, numcell::NumCell,
object_drop_queue::ObjectDropQueue,
oserror::OsError, oserror::OsError,
queue::AsyncQueue, queue::AsyncQueue,
rc_eq::RcEq, rc_eq::RcEq,
@ -390,6 +392,9 @@ fn start_compositor2(
supports_presentation_feedback: Default::default(), supports_presentation_feedback: Default::default(),
eventfd_cache, eventfd_cache,
lazy_event_sources: Default::default(), 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)); state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state); create_dummy_output(&state);
@ -414,6 +419,7 @@ fn start_compositor2(
let _compositor = engine.spawn("compositor", start_compositor3(state.clone(), test_future)); let _compositor = engine.spawn("compositor", start_compositor3(state.clone(), test_future));
ring.run()?; ring.run()?;
state.clear(); state.clear();
engine.clear();
Ok(()) Ok(())
} }
@ -594,6 +600,10 @@ fn start_global_event_handlers(state: &Rc<State>) -> Vec<SpawnedFuture<()>> {
"lazy event sources", "lazy event sources",
handle_lazy_event_sources(state.clone()), 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(), wlr_output_heads: Default::default(),
}); });
let schedule = Rc::new(OutputSchedule::new( let schedule = Rc::new(OutputSchedule::new(
&state.ring, state,
&state.eng,
&connector_data, &connector_data,
&persistent_state, &persistent_state,
)); ));

View file

@ -478,7 +478,7 @@ impl ConfigProxyHandler {
} else { } else {
Some(self.get_keymap(keymap)?) Some(self.get_keymap(keymap)?)
}; };
dev.set_keymap(map); dev.set_keymap(&self.state, map);
Ok(()) Ok(())
} }
@ -532,13 +532,13 @@ impl ConfigProxyHandler {
) -> Result<(), CphError> { ) -> Result<(), CphError> {
let dev = self.get_device_handler_data(input_device)?; let dev = self.get_device_handler_data(input_device)?;
let output = self.get_output_node(connector)?; let output = self.get_output_node(connector)?;
dev.set_output(Some(&output.global)); dev.set_output(&self.state, Some(&output.global));
Ok(()) Ok(())
} }
fn handle_remove_input_mapping(&self, input_device: InputDevice) -> Result<(), CphError> { fn handle_remove_input_mapping(&self, input_device: InputDevice) -> Result<(), CphError> {
let dev = self.get_device_handler_data(input_device)?; let dev = self.get_device_handler_data(input_device)?;
dev.set_output(None); dev.set_output(&self.state, None);
Ok(()) Ok(())
} }
@ -788,7 +788,7 @@ impl ConfigProxyHandler {
Some(self.get_seat(seat)?) Some(self.get_seat(seat)?)
}; };
let dev = self.get_device_handler_data(device)?; let dev = self.get_device_handler_data(device)?;
dev.set_seat(seat); dev.set_seat(&self.state, seat);
Ok(()) Ok(())
} }
@ -798,7 +798,7 @@ impl ConfigProxyHandler {
left_handed: bool, left_handed: bool,
) -> Result<(), CphError> { ) -> Result<(), CphError> {
let dev = self.get_device_handler_data(device)?; let dev = self.get_device_handler_data(device)?;
dev.set_left_handed(left_handed); dev.set_left_handed(&self.state, left_handed);
Ok(()) Ok(())
} }
@ -813,31 +813,31 @@ impl ConfigProxyHandler {
ACCEL_PROFILE_ADAPTIVE => InputDeviceAccelProfile::Adaptive, ACCEL_PROFILE_ADAPTIVE => InputDeviceAccelProfile::Adaptive,
_ => return Err(CphError::UnknownAccelProfile(accel_profile)), _ => return Err(CphError::UnknownAccelProfile(accel_profile)),
}; };
dev.set_accel_profile(profile); dev.set_accel_profile(&self.state, profile);
Ok(()) Ok(())
} }
fn handle_set_accel_speed(&self, device: InputDevice, speed: f64) -> Result<(), CphError> { fn handle_set_accel_speed(&self, device: InputDevice, speed: f64) -> Result<(), CphError> {
let dev = self.get_device_handler_data(device)?; let dev = self.get_device_handler_data(device)?;
dev.set_accel_speed(speed); dev.set_accel_speed(&self.state, speed);
Ok(()) Ok(())
} }
fn handle_set_px_per_wheel_scroll(&self, device: InputDevice, px: f64) -> Result<(), CphError> { fn handle_set_px_per_wheel_scroll(&self, device: InputDevice, px: f64) -> Result<(), CphError> {
let dev = self.get_device_handler_data(device)?; 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(()) Ok(())
} }
fn handle_set_tap_enabled(&self, device: InputDevice, enabled: bool) -> Result<(), CphError> { fn handle_set_tap_enabled(&self, device: InputDevice, enabled: bool) -> Result<(), CphError> {
let dev = self.get_device_handler_data(device)?; let dev = self.get_device_handler_data(device)?;
dev.set_tap_enabled(enabled); dev.set_tap_enabled(&self.state, enabled);
Ok(()) Ok(())
} }
fn handle_set_drag_enabled(&self, device: InputDevice, enabled: bool) -> Result<(), CphError> { fn handle_set_drag_enabled(&self, device: InputDevice, enabled: bool) -> Result<(), CphError> {
let dev = self.get_device_handler_data(device)?; let dev = self.get_device_handler_data(device)?;
dev.set_drag_enabled(enabled); dev.set_drag_enabled(&self.state, enabled);
Ok(()) Ok(())
} }
@ -847,7 +847,7 @@ impl ConfigProxyHandler {
enabled: bool, enabled: bool,
) -> Result<(), CphError> { ) -> Result<(), CphError> {
let dev = self.get_device_handler_data(device)?; let dev = self.get_device_handler_data(device)?;
dev.set_natural_scrolling_enabled(enabled); dev.set_natural_scrolling_enabled(&self.state, enabled);
Ok(()) Ok(())
} }
@ -857,7 +857,7 @@ impl ConfigProxyHandler {
enabled: bool, enabled: bool,
) -> Result<(), CphError> { ) -> Result<(), CphError> {
let dev = self.get_device_handler_data(device)?; let dev = self.get_device_handler_data(device)?;
dev.set_drag_lock_enabled(enabled); dev.set_drag_lock_enabled(&self.state, enabled);
Ok(()) Ok(())
} }
@ -867,7 +867,7 @@ impl ConfigProxyHandler {
matrix: [[f64; 2]; 2], matrix: [[f64; 2]; 2],
) -> Result<(), CphError> { ) -> Result<(), CphError> {
let dev = self.get_device_handler_data(device)?; let dev = self.get_device_handler_data(device)?;
dev.set_transform_matrix(matrix); dev.set_transform_matrix(&self.state, matrix);
Ok(()) Ok(())
} }
@ -877,7 +877,7 @@ impl ConfigProxyHandler {
matrix: [[f32; 3]; 2], matrix: [[f32; 3]; 2],
) -> Result<(), CphError> { ) -> Result<(), CphError> {
let dev = self.get_device_handler_data(device)?; let dev = self.get_device_handler_data(device)?;
dev.set_calibration_matrix(matrix); dev.set_calibration_matrix(&self.state, matrix);
Ok(()) Ok(())
} }
@ -893,7 +893,7 @@ impl ConfigProxyHandler {
CLICK_METHOD_CLICKFINGER => InputDeviceClickMethod::Clickfinger, CLICK_METHOD_CLICKFINGER => InputDeviceClickMethod::Clickfinger,
_ => return Err(CphError::UnknownClickMethod(click_method)), _ => return Err(CphError::UnknownClickMethod(click_method)),
}; };
dev.set_click_method(method); dev.set_click_method(&self.state, method);
Ok(()) Ok(())
} }
@ -903,7 +903,7 @@ impl ConfigProxyHandler {
enabled: bool, enabled: bool,
) -> Result<(), CphError> { ) -> Result<(), CphError> {
let dev = self.get_device_handler_data(device)?; 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(()) Ok(())
} }
@ -951,8 +951,10 @@ impl ConfigProxyHandler {
} }
fn handle_set_flip_margin(&self, device: DrmDevice, margin: Duration) -> Result<(), CphError> { fn handle_set_flip_margin(&self, device: DrmDevice, margin: Duration) -> Result<(), CphError> {
self.get_drm_device(device)? self.get_drm_device(device)?.set_flip_margin(
.set_flip_margin(margin.as_nanos().try_into().unwrap_or(u64::MAX)); &self.state,
margin.as_nanos().try_into().unwrap_or(u64::MAX),
);
Ok(()) Ok(())
} }
@ -987,7 +989,7 @@ impl ConfigProxyHandler {
match device { match device {
Some(dev) => self Some(dev) => self
.get_drm_device(dev)? .get_drm_device(dev)?
.set_direct_scanout_enabled(enabled), .set_direct_scanout_enabled(&self.state, enabled),
_ => self.state.direct_scanout_enabled.set(enabled), _ => self.state.direct_scanout_enabled.set(enabled),
} }
Ok(()) Ok(())
@ -1123,11 +1125,11 @@ impl ConfigProxyHandler {
} }
fn handle_set_idle(&self, timeout: Duration) { 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) { 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) { fn handle_set_explicit_sync_enabled(&self, enabled: bool) {
@ -1484,7 +1486,7 @@ impl ConfigProxyHandler {
match connector { match connector {
Some(c) => { Some(c) => {
let connector = self.get_output_node(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 { let Some((hz, _)) = map_cursor_hz(hz) else {
@ -1807,6 +1809,16 @@ impl ConfigProxyHandler {
Ok(()) 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) { fn handle_set_log_level(&self, level: ConfigLogLevel) {
self.state.set_log_level(level.into()); self.state.set_log_level(level.into());
} }
@ -3311,6 +3323,11 @@ impl ConfigProxyHandler {
fds, fds,
tag, tag,
} => self.handle_run(prog, args, env, fds, tag).wrn("run")?, } => 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(()) 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)) self.root(ClmMatchTag::new(string))
} }
#[expect(dead_code)]
pub fn id(&self, id: ClientId) -> Rc<ClmUpstreamNode> { pub fn id(&self, id: ClientId) -> Rc<ClmUpstreamNode> {
self.root(ClmMatchId(id)) self.root(ClmMatchId(id))
} }

View file

@ -1,6 +1,7 @@
use { use {
crate::{ crate::{
backend::HardwareCursorUpdate, backend::HardwareCursorUpdate,
control_center::CCI_INPUT,
cursor::{Cursor, DEFAULT_CURSOR_SIZE, KnownCursor}, cursor::{Cursor, DEFAULT_CURSOR_SIZE, KnownCursor},
fixed::Fixed, fixed::Fixed,
gfx_api::{AcquireSync, ReleaseSync}, gfx_api::{AcquireSync, ReleaseSync},
@ -183,6 +184,7 @@ impl CursorUserGroup {
self.remove_hardware_cursor(); self.remove_hardware_cursor();
self.state.cursor_user_group_hardware_cursor.take(); self.state.cursor_user_group_hardware_cursor.take();
} }
self.state.trigger_cci(CCI_INPUT);
} }
pub fn hardware_cursor(&self) -> bool { pub fn hardware_cursor(&self) -> bool {
@ -195,10 +197,10 @@ impl CursorUserGroup {
self.state.remove_cursor_size(old); self.state.remove_cursor_size(old);
self.state.add_cursor_size(size); self.state.add_cursor_size(size);
self.reload_known_cursor(); self.reload_known_cursor();
self.state.trigger_cci(CCI_INPUT);
} }
} }
#[expect(dead_code)]
pub fn cursor_size(&self) -> u32 { pub fn cursor_size(&self) -> u32 {
self.size.get() 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)] #[derive(Debug)]
#[expect(dead_code)]
pub struct Font { pub struct Font {
pub fullname: String, pub fullname: String,
pub file: PathBuf, pub file: PathBuf,
pub index: Option<i32>, pub index: Option<i32>,
} }
#[expect(dead_code)]
pub fn match_font(family: &str) -> Result<Font, FontconfigError> { pub fn match_font(family: &str) -> Result<Font, FontconfigError> {
thread_local! { thread_local! {
static CONFIG: *mut FcConfig = FcConfigGetCurrent(); static CONFIG: *mut FcConfig = FcConfigGetCurrent();

View file

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

View file

@ -21,6 +21,7 @@ pub mod jay_ei_session_builder;
pub mod jay_idle; pub mod jay_idle;
pub mod jay_input; pub mod jay_input;
pub mod jay_log_file; pub mod jay_log_file;
pub mod jay_open_control_center_request;
pub mod jay_output; pub mod jay_output;
pub mod jay_pointer; pub mod jay_pointer;
pub mod jay_popup_ext_manager_v1; pub mod jay_popup_ext_manager_v1;

View file

@ -105,14 +105,13 @@ pub struct ReadOnlyHeadState {
} }
impl ReadOnlyHeadState { impl ReadOnlyHeadState {
#[expect(dead_code)]
pub fn borrow(&self) -> Ref<'_, HeadState> { pub fn borrow(&self) -> Ref<'_, HeadState> {
self.state.borrow() self.state.borrow()
} }
} }
impl HeadState { 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.in_compositor_space = false;
self.wl_output = None; self.wl_output = None;
if !self.connector_enabled { if !self.connector_enabled {
@ -131,7 +130,7 @@ impl HeadState {
self.wl_output = wl_output; self.wl_output = wl_output;
} }
fn update_size(&mut self) { pub fn update_size(&mut self) {
self.size = self.size =
OutputNode::calculate_extents_(self.mode, self.transform, self.scale, self.position) OutputNode::calculate_extents_(self.mode, self.transform, self.scale, self.position)
.size(); .size();
@ -213,7 +212,7 @@ pub enum HeadCommonError {
} }
pub struct HeadManagers { pub struct HeadManagers {
name: HeadName, pub name: HeadName,
state: Rc<RefCell<HeadState>>, state: Rc<RefCell<HeadState>>,
managers: CopyHashMap<(ClientId, JayHeadManagerSessionV1Id), Rc<Head>>, managers: CopyHashMap<(ClientId, JayHeadManagerSessionV1Id), Rc<Head>>,
} }
@ -235,7 +234,6 @@ impl HeadManagers {
} }
} }
#[expect(dead_code)]
pub fn state(&self) -> ReadOnlyHeadState { pub fn state(&self) -> ReadOnlyHeadState {
ReadOnlyHeadState { ReadOnlyHeadState {
state: self.state.clone(), state: self.state.clone(),

View file

@ -11,6 +11,7 @@ use {
jay_idle::JayIdle, jay_idle::JayIdle,
jay_input::JayInput, jay_input::JayInput,
jay_log_file::JayLogFile, jay_log_file::JayLogFile,
jay_open_control_center_request::JayOpenControlCenterRequest,
jay_output::JayOutput, jay_output::JayOutput,
jay_pointer::JayPointer, jay_pointer::JayPointer,
jay_randr::JayRandr, jay_randr::JayRandr,
@ -77,7 +78,7 @@ global_base!(JayCompositorGlobal, JayCompositor, JayCompositorError);
impl Global for JayCompositorGlobal { impl Global for JayCompositorGlobal {
fn version(&self) -> u32 { fn version(&self) -> u32 {
27 28
} }
fn required_caps(&self) -> ClientCaps { fn required_caps(&self) -> ClientCaps {
@ -541,6 +542,25 @@ impl JayCompositorRequestHandler for JayCompositor {
}); });
Ok(()) 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! { object_base! {

View file

@ -68,14 +68,14 @@ impl JayIdleRequestHandler for JayIdle {
fn set_interval(&self, req: SetInterval, _slf: &Rc<Self>) -> Result<(), Self::Error> { fn set_interval(&self, req: SetInterval, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let interval = Duration::from_secs(req.interval); let interval = Duration::from_secs(req.interval);
let state = &self.client.state; let state = &self.client.state;
state.idle.set_timeout(interval); state.idle.set_timeout(state, interval);
Ok(()) Ok(())
} }
fn set_grace_period(&self, req: SetGracePeriod, _slf: &Rc<Self>) -> Result<(), Self::Error> { fn set_grace_period(&self, req: SetGracePeriod, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let period = Duration::from_secs(req.period); let period = Duration::from_secs(req.period);
let state = &self.client.state; let state = &self.client.state;
state.idle.set_grace_period(period); state.idle.set_grace_period(state, period);
Ok(()) Ok(())
} }
} }

View file

@ -14,7 +14,7 @@ use {
LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER, LIBINPUT_CONFIG_CLICK_METHOD_NONE, LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER, LIBINPUT_CONFIG_CLICK_METHOD_NONE,
}, },
object::{Object, Version}, object::{Object, Version},
state::{DeviceHandlerData, InputDeviceData}, state::{DeviceHandlerData, InputDeviceData, State},
utils::errorfmt::ErrorFmt, utils::errorfmt::ErrorFmt,
wire::{JayInputId, jay_input::*}, wire::{JayInputId, jay_input::*},
}, },
@ -28,6 +28,7 @@ use {
pub struct JayInput { pub struct JayInput {
pub id: JayInputId, pub id: JayInputId,
pub client: Rc<Client>, pub client: Rc<Client>,
pub state: Rc<State>,
pub tracker: Tracker<Self>, pub tracker: Tracker<Self>,
pub version: Version, pub version: Version,
} }
@ -41,6 +42,7 @@ impl JayInput {
Self { Self {
id, id,
client: client.clone(), client: client.clone(),
state: client.state.clone(),
tracker: Default::default(), tracker: Default::default(),
version, version,
} }
@ -309,7 +311,7 @@ impl JayInputRequestHandler for JayInput {
LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE => InputDeviceAccelProfile::Adaptive, LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE => InputDeviceAccelProfile::Adaptive,
_ => return Err(JayInputError::UnknownAccelerationProfile(req.profile)), _ => return Err(JayInputError::UnknownAccelerationProfile(req.profile)),
}; };
dev.set_accel_profile(profile); dev.set_accel_profile(&self.state, profile);
Ok(()) Ok(())
}) })
} }
@ -317,7 +319,7 @@ impl JayInputRequestHandler for JayInput {
fn set_accel_speed(&self, req: SetAccelSpeed, _slf: &Rc<Self>) -> Result<(), Self::Error> { fn set_accel_speed(&self, req: SetAccelSpeed, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.or_error(|| { self.or_error(|| {
let dev = self.device(req.id)?; let dev = self.device(req.id)?;
dev.set_accel_speed(req.speed); dev.set_accel_speed(&self.state, req.speed);
Ok(()) Ok(())
}) })
} }
@ -325,7 +327,7 @@ impl JayInputRequestHandler for JayInput {
fn set_tap_enabled(&self, req: SetTapEnabled, _slf: &Rc<Self>) -> Result<(), Self::Error> { fn set_tap_enabled(&self, req: SetTapEnabled, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.or_error(|| { self.or_error(|| {
let dev = self.device(req.id)?; let dev = self.device(req.id)?;
dev.set_tap_enabled(req.enabled != 0); dev.set_tap_enabled(&self.state, req.enabled != 0);
Ok(()) Ok(())
}) })
} }
@ -337,7 +339,7 @@ impl JayInputRequestHandler for JayInput {
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
self.or_error(|| { self.or_error(|| {
let dev = self.device(req.id)?; let dev = self.device(req.id)?;
dev.set_drag_enabled(req.enabled != 0); dev.set_drag_enabled(&self.state, req.enabled != 0);
Ok(()) Ok(())
}) })
} }
@ -349,7 +351,7 @@ impl JayInputRequestHandler for JayInput {
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
self.or_error(|| { self.or_error(|| {
let dev = self.device(req.id)?; 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(()) Ok(())
}) })
} }
@ -357,7 +359,7 @@ impl JayInputRequestHandler for JayInput {
fn set_left_handed(&self, req: SetLeftHanded, _slf: &Rc<Self>) -> Result<(), Self::Error> { fn set_left_handed(&self, req: SetLeftHanded, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.or_error(|| { self.or_error(|| {
let dev = self.device(req.id)?; let dev = self.device(req.id)?;
dev.set_left_handed(req.enabled != 0); dev.set_left_handed(&self.state, req.enabled != 0);
Ok(()) Ok(())
}) })
} }
@ -369,7 +371,7 @@ impl JayInputRequestHandler for JayInput {
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
self.or_error(|| { self.or_error(|| {
let dev = self.device(req.id)?; 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(()) Ok(())
}) })
} }
@ -381,7 +383,7 @@ impl JayInputRequestHandler for JayInput {
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
self.or_error(|| { self.or_error(|| {
let dev = self.device(req.id)?; 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(()) Ok(())
}) })
} }
@ -393,7 +395,7 @@ impl JayInputRequestHandler for JayInput {
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
self.or_error(|| { self.or_error(|| {
let dev = self.device(req.id)?; 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(()) Ok(())
}) })
} }
@ -410,7 +412,7 @@ impl JayInputRequestHandler for JayInput {
self.or_error(|| { self.or_error(|| {
let seat = self.seat(req.seat)?; let seat = self.seat(req.seat)?;
let dev = self.device(req.id)?; let dev = self.device(req.id)?;
dev.set_seat(Some(seat)); dev.set_seat(&self.state, Some(seat));
Ok(()) Ok(())
}) })
} }
@ -418,7 +420,7 @@ impl JayInputRequestHandler for JayInput {
fn detach(&self, req: Detach, _slf: &Rc<Self>) -> Result<(), Self::Error> { fn detach(&self, req: Detach, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.or_error(|| { self.or_error(|| {
let dev = self.device(req.id)?; let dev = self.device(req.id)?;
dev.set_seat(None); dev.set_seat(&self.state, None);
Ok(()) Ok(())
}) })
} }
@ -459,7 +461,7 @@ impl JayInputRequestHandler for JayInput {
fn set_device_keymap(&self, req: SetDeviceKeymap, _slf: &Rc<Self>) -> Result<(), Self::Error> { fn set_device_keymap(&self, req: SetDeviceKeymap, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.set_keymap_impl(&req.keymap, req.keymap_len, |map| { self.set_keymap_impl(&req.keymap, req.keymap_len, |map| {
let dev = self.device(req.id)?; let dev = self.device(req.id)?;
dev.set_keymap(Some(map.clone())); dev.set_keymap(&self.state, Some(map.clone()));
Ok(()) Ok(())
}) })
} }
@ -490,11 +492,11 @@ impl JayInputRequestHandler for JayInput {
.find(|c| c.global.connector.name.to_ascii_lowercase() == namelc) .find(|c| c.global.connector.name.to_ascii_lowercase() == namelc)
.cloned(); .cloned();
match c { match c {
Some(c) => dev.set_output(Some(&c.global)), Some(c) => dev.set_output(&self.state, Some(&c.global)),
_ => return Err(JayInputError::OutputNotConnected), _ => return Err(JayInputError::OutputNotConnected),
} }
} }
_ => dev.set_output(None), _ => dev.set_output(&self.state, None),
} }
Ok(()) Ok(())
}) })
@ -507,7 +509,10 @@ impl JayInputRequestHandler for JayInput {
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
self.or_error(|| { self.or_error(|| {
let dev = self.device(req.id)?; 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(()) Ok(())
}) })
} }
@ -521,7 +526,7 @@ impl JayInputRequestHandler for JayInput {
LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER => InputDeviceClickMethod::Clickfinger, LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER => InputDeviceClickMethod::Clickfinger,
_ => return Err(JayInputError::UnknownClickMethod(req.method)), _ => return Err(JayInputError::UnknownClickMethod(req.method)),
}; };
dev.set_click_method(method); dev.set_click_method(&self.state, method);
Ok(()) Ok(())
}) })
} }
@ -533,7 +538,7 @@ impl JayInputRequestHandler for JayInput {
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
self.or_error(|| { self.or_error(|| {
let dev = self.device(req.id)?; 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(()) Ok(())
}) })
} }
@ -594,7 +599,7 @@ impl JayInputRequestHandler for JayInput {
req.options, req.options,
|map| { |map| {
let dev = self.device(req.id)?; let dev = self.device(req.id)?;
dev.set_keymap(Some(map.clone())); dev.set_keymap(&self.state, Some(map.clone()));
Ok(()) 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 { let Some(dev) = self.get_device(req.dev) else {
return Ok(()); return Ok(());
}; };
dev.set_direct_scanout_enabled(req.enabled != 0); dev.set_direct_scanout_enabled(&self.state, req.enabled != 0);
Ok(()) Ok(())
} }
@ -456,7 +456,7 @@ impl JayRandrRequestHandler for JayRandr {
let Some(c) = self.get_output_node(req.output) else { let Some(c) = self.get_output_node(req.output) else {
return Ok(()); return Ok(());
}; };
c.schedule.set_cursor_hz(req.hz); c.schedule.set_cursor_hz(&self.state, req.hz);
Ok(()) Ok(())
} }
@ -493,7 +493,7 @@ impl JayRandrRequestHandler for JayRandr {
let Some(dev) = self.get_device(req.dev) else { let Some(dev) = self.get_device(req.dev) else {
return Ok(()); return Ok(());
}; };
dev.set_flip_margin(req.margin_ns); dev.set_flip_margin(&self.state, req.margin_ns);
Ok(()) Ok(())
} }

View file

@ -28,6 +28,7 @@ use {
ButtonState, InputDeviceAccelProfile, InputDeviceClickMethod, Leds, TransformMatrix, ButtonState, InputDeviceAccelProfile, InputDeviceClickMethod, Leds, TransformMatrix,
}, },
client::{Client, ClientError, ClientId}, client::{Client, ClientError, ClientId},
control_center::CCI_INPUT,
cursor_user::{CursorUser, CursorUserGroup, CursorUserOwner}, cursor_user::{CursorUser, CursorUserGroup, CursorUserOwner},
ei::ei_ifs::ei_seat::EiSeat, ei::ei_ifs::ei_seat::EiSeat,
fixed::Fixed, fixed::Fixed,
@ -98,6 +99,7 @@ use {
numcell::NumCell, numcell::NumCell,
rc_eq::{rc_eq, rc_weak_eq}, rc_eq::{rc_eq, rc_weak_eq},
smallmap::SmallMap, smallmap::SmallMap,
static_text::StaticText,
}, },
wire::{ wire::{
ExtIdleNotificationV1Id, WlDataDeviceId, WlKeyboardId, WlPointerId, WlSeatId, ExtIdleNotificationV1Id, WlDataDeviceId, WlKeyboardId, WlPointerId, WlSeatId,
@ -138,6 +140,9 @@ const MISSING_CAPABILITY: u32 = 0;
pub const BTN_LEFT: u32 = 0x110; pub const BTN_LEFT: u32 = 0x110;
pub const BTN_RIGHT: u32 = 0x111; 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); pub const SEAT_NAME_SINCE: Version = Version(2);
@ -273,6 +278,15 @@ pub enum FallbackOutputMode {
Focus, Focus,
} }
impl StaticText for FallbackOutputMode {
fn text(&self) -> &'static str {
match self {
FallbackOutputMode::Cursor => "Cursor",
FallbackOutputMode::Focus => "Focus",
}
}
}
impl TryFrom<ConfigFallbackOutputMode> for FallbackOutputMode { impl TryFrom<ConfigFallbackOutputMode> for FallbackOutputMode {
type Error = (); type Error = ();
@ -765,6 +779,7 @@ impl WlSeatGlobal {
if let Some(grab) = self.input_method_grab.get() { if let Some(grab) = self.input_method_grab.get() {
grab.on_repeat_info(); grab.on_repeat_info();
} }
self.state.trigger_cci(CCI_INPUT);
} }
pub fn close(self: &Rc<Self>) { pub fn close(self: &Rc<Self>) {
@ -960,18 +975,18 @@ impl WlSeatGlobal {
pub fn focus_history_set_visible(&self, visible: bool) { pub fn focus_history_set_visible(&self, visible: bool) {
self.focus_history_visible_only.set(visible); self.focus_history_visible_only.set(visible);
self.state.trigger_cci(CCI_INPUT);
} }
#[expect(dead_code)]
pub fn focus_history_visible(&self) -> bool { pub fn focus_history_visible(&self) -> bool {
self.focus_history_visible_only.get() self.focus_history_visible_only.get()
} }
pub fn focus_history_set_same_workspace(&self, same_workspace: bool) { pub fn focus_history_set_same_workspace(&self, same_workspace: bool) {
self.focus_history_same_workspace.set(same_workspace); 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 { pub fn focus_history_same_workspace(&self) -> bool {
self.focus_history_same_workspace.get() self.focus_history_same_workspace.get()
} }
@ -1476,18 +1491,18 @@ impl WlSeatGlobal {
pub fn set_focus_follows_mouse(&self, focus_follows_mouse: bool) { pub fn set_focus_follows_mouse(&self, focus_follows_mouse: bool) {
self.focus_follows_mouse.set(focus_follows_mouse); self.focus_follows_mouse.set(focus_follows_mouse);
self.state.trigger_cci(CCI_INPUT);
} }
#[expect(dead_code)]
pub fn focus_follows_mouse(&self) -> bool { pub fn focus_follows_mouse(&self) -> bool {
self.focus_follows_mouse.get() self.focus_follows_mouse.get()
} }
pub fn set_fallback_output_mode(&self, fallback_output_mode: FallbackOutputMode) { pub fn set_fallback_output_mode(&self, fallback_output_mode: FallbackOutputMode) {
self.fallback_output_mode.set(fallback_output_mode); self.fallback_output_mode.set(fallback_output_mode);
self.state.trigger_cci(CCI_INPUT);
} }
#[expect(dead_code)]
pub fn fallback_output_mode(&self) -> FallbackOutputMode { pub fn fallback_output_mode(&self) -> FallbackOutputMode {
self.fallback_output_mode.get() self.fallback_output_mode.get()
} }
@ -1607,9 +1622,9 @@ impl WlSeatGlobal {
pub fn set_pointer_revert_key(&self, key: KeySym) { pub fn set_pointer_revert_key(&self, key: KeySym) {
self.revert_key.set(key); self.revert_key.set(key);
self.state.trigger_cci(CCI_INPUT);
} }
#[expect(dead_code)]
pub fn pointer_revert_key(&self) -> KeySym { pub fn pointer_revert_key(&self) -> KeySym {
self.revert_key.get() self.revert_key.get()
} }
@ -1806,7 +1821,7 @@ pub fn collect_kb_foci(node: Rc<dyn Node>) -> SmallVec<[Rc<WlSeatGlobal>; 3]> {
} }
impl DeviceHandlerData { 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(new) = &seat {
if let Some(old) = self.seat.get() if let Some(old) = self.seat.get()
&& old.id() == new.id() && old.id() == new.id()
@ -1845,6 +1860,7 @@ impl DeviceHandlerData {
} }
} }
self.attach_event_listeners(); self.attach_event_listeners();
state.trigger_cci(CCI_INPUT);
} }
fn destroy_physical_keyboard_state(&self) { 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.destroy_physical_keyboard_state();
self.keymap.set(keymap); self.keymap.set(keymap);
self.attach_event_listeners(); 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 { match output {
None => { None => {
log::info!("Removing output mapping of {}", self.device.name()); log::info!("Removing output mapping of {}", self.device.name());
@ -1883,6 +1900,7 @@ impl DeviceHandlerData {
self.output.set(Some(o.opt.clone())); self.output.set(Some(o.opt.clone()));
} }
} }
state.trigger_cci(CCI_INPUT);
} }
pub fn get_rect(&self, state: &State) -> Rect { pub fn get_rect(&self, state: &State) -> Rect {
@ -1894,52 +1912,64 @@ impl DeviceHandlerData {
state.root.extents.get() 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); self.device.set_middle_button_emulation_enabled(v);
state.trigger_cci(CCI_INPUT);
} }
} }

View file

@ -1,6 +1,7 @@
use { use {
crate::{ crate::{
backend::KeyState, backend::KeyState,
control_center::CCI_INPUT,
ifs::{ ifs::{
wl_seat::{ wl_seat::{
WlSeatGlobal, WlSeatGlobal,
@ -89,6 +90,7 @@ impl WlSeatGlobal {
im.cancel_simple(self); im.cancel_simple(self);
} }
} }
self.state.trigger_cci(CCI_INPUT);
} }
pub fn simple_im_enabled(&self) -> bool { pub fn simple_im_enabled(&self) -> bool {

View file

@ -44,12 +44,12 @@ impl ZwpIdleInhibitorV1 {
pub fn activate(self: &Rc<Self>) { pub fn activate(self: &Rc<Self>) {
let state = &self.client.state; let state = &self.client.state;
state.idle.add_inhibitor(self); state.idle.add_inhibitor(state, self);
} }
pub fn deactivate(&self) { pub fn deactivate(&self) {
let state = &self.client.state; 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 id: KbvmMapId,
pub state_machine: StateMachine, pub state_machine: StateMachine,
pub lookup_table: LookupTable, pub lookup_table: LookupTable,
#[expect(dead_code)]
pub map_text: String, pub map_text: String,
pub map: KeymapFd, pub map: KeymapFd,
pub xwayland_map: KeymapFd, pub xwayland_map: KeymapFd,

View file

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

View file

@ -58,6 +58,7 @@ mod clientmem;
mod cmm; mod cmm;
mod compositor; mod compositor;
mod config; mod config;
mod control_center;
mod copy_device; mod copy_device;
mod cpu_worker; mod cpu_worker;
mod criteria; mod criteria;
@ -67,6 +68,7 @@ mod damage;
mod dbus; mod dbus;
mod drm_feedback; mod drm_feedback;
mod edid; mod edid;
mod egui_adapter;
mod ei; mod ei;
mod eventfd_cache; mod eventfd_cache;
mod fixed; mod fixed;

View file

@ -2,9 +2,10 @@ use {
crate::{ crate::{
async_engine::AsyncEngine, async_engine::AsyncEngine,
backend::HardwareCursor, backend::HardwareCursor,
control_center::CCI_OUTPUTS,
ifs::wl_output::PersistentOutputState, ifs::wl_output::PersistentOutputState,
io_uring::{IoUring, IoUringError}, io_uring::{IoUring, IoUringError},
state::ConnectorData, state::{ConnectorData, State},
utils::{ utils::{
asyncevent::AsyncEvent, cell_ext::CellExt, clonecell::CloneCell, errorfmt::ErrorFmt, asyncevent::AsyncEvent, cell_ext::CellExt, clonecell::CloneCell, errorfmt::ErrorFmt,
numcell::NumCell, numcell::NumCell,
@ -51,8 +52,7 @@ pub struct OutputSchedule {
impl OutputSchedule { impl OutputSchedule {
pub fn new( pub fn new(
ring: &Rc<IoUring>, state: &State,
eng: &Rc<AsyncEngine>,
connector: &Rc<ConnectorData>, connector: &Rc<ConnectorData>,
persistent: &Rc<PersistentOutputState>, persistent: &Rc<PersistentOutputState>,
) -> Self { ) -> Self {
@ -60,8 +60,8 @@ impl OutputSchedule {
changed: Default::default(), changed: Default::default(),
run: Default::default(), run: Default::default(),
connector: connector.clone(), connector: connector.clone(),
ring: ring.clone(), ring: state.ring.clone(),
eng: eng.clone(), eng: state.eng.clone(),
vrr_enabled: Default::default(), vrr_enabled: Default::default(),
hardware_cursor_change: Cell::new(Change::None), hardware_cursor_change: Cell::new(Change::None),
software_cursor_change: Cell::new(Change::None), software_cursor_change: Cell::new(Change::None),
@ -72,7 +72,7 @@ impl OutputSchedule {
iteration: Default::default(), iteration: Default::default(),
}; };
if let Some(hz) = persistent.vrr_cursor_hz.get() { if let Some(hz) = persistent.vrr_cursor_hz.get() {
slf.set_cursor_hz(hz); slf.set_cursor_hz(state, hz);
} }
slf slf
} }
@ -118,7 +118,7 @@ impl OutputSchedule {
self.trigger(); 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) { let (hz, delta) = match map_cursor_hz(hz) {
None => { None => {
log::warn!("Ignoring cursor frequency {hz}"); log::warn!("Ignoring cursor frequency {hz}");
@ -128,6 +128,7 @@ impl OutputSchedule {
}; };
self.persistent.vrr_cursor_hz.set(hz); self.persistent.vrr_cursor_hz.set(hz);
self.connector.head_managers.handle_cursor_hz_change(hz); self.connector.head_managers.handle_cursor_hz_change(hz);
state.trigger_cci(CCI_OUTPUTS);
self.cursor_delta_nsec.set(delta); self.cursor_delta_nsec.set(delta);
self.trigger(); self.trigger();
} }

View file

@ -42,6 +42,15 @@ pub struct AcceptorMetadata {
pub tag: Option<String>, pub tag: Option<String>,
} }
impl AcceptorMetadata {
pub fn secure() -> Self {
Self {
secure: true,
..Default::default()
}
}
}
impl SecurityContextAcceptors { impl SecurityContextAcceptors {
pub fn clear(&self) { pub fn clear(&self) {
for acceptor in self.acceptors.lock().drain_values() { for acceptor in self.acceptors.lock().drain_values() {

View file

@ -1,6 +1,7 @@
use { use {
crate::{ crate::{
acceptor::Acceptor, acceptor::Acceptor,
allocator::BufferObject,
async_engine::{AsyncEngine, SpawnedFuture}, async_engine::{AsyncEngine, SpawnedFuture},
backend::{ backend::{
Backend, BackendConnectorState, BackendConnectorStateSerials, BackendDrmDevice, Backend, BackendConnectorState, BackendConnectorStateSerials, BackendDrmDevice,
@ -15,6 +16,10 @@ use {
cmm::{cmm_description::ColorDescription, cmm_manager::ColorManager}, cmm::{cmm_description::ColorDescription, cmm_manager::ColorManager},
compositor::{LIBEI_SOCKET, LogLevel}, compositor::{LIBEI_SOCKET, LogLevel},
config::ConfigProxy, 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, copy_device::CopyDeviceRegistry,
cpu_worker::CpuWorker, cpu_worker::CpuWorker,
criteria::{clm::ClMatcherManager, tlm::TlMatcherManager}, criteria::{clm::ClMatcherManager, tlm::TlMatcherManager},
@ -23,6 +28,7 @@ use {
damage::DamageVisualizer, damage::DamageVisualizer,
dbus::Dbus, dbus::Dbus,
drm_feedback::{DrmFeedback, DrmFeedbackIds}, drm_feedback::{DrmFeedback, DrmFeedbackIds},
egui_adapter::egui_platform::EggState,
ei::{ ei::{
ei_acceptor::EiAcceptor, ei_acceptor::EiAcceptor,
ei_client::{EiClient, EiClients}, ei_client::{EiClient, EiClients},
@ -115,6 +121,7 @@ use {
hash_map_ext::HashMapExt, hash_map_ext::HashMapExt,
linkedlist::LinkedList, linkedlist::LinkedList,
numcell::NumCell, numcell::NumCell,
object_drop_queue::ObjectDropQueue,
queue::AsyncQueue, queue::AsyncQueue,
refcounted::RefCounted, refcounted::RefCounted,
run_toplevel::RunToplevel, run_toplevel::RunToplevel,
@ -224,7 +231,7 @@ pub struct State {
pub activation_tokens: CopyHashMap<ActivationToken, ()>, pub activation_tokens: CopyHashMap<ActivationToken, ()>,
pub toplevel_lists: pub toplevel_lists:
CopyHashMap<(ClientId, ExtForeignToplevelListV1Id), Rc<ExtForeignToplevelListV1>>, CopyHashMap<(ClientId, ExtForeignToplevelListV1Id), Rc<ExtForeignToplevelListV1>>,
pub dma_buf_ids: DmaBufIds, pub dma_buf_ids: Rc<DmaBufIds>,
pub drm_feedback_ids: DrmFeedbackIds, pub drm_feedback_ids: DrmFeedbackIds,
pub direct_scanout_enabled: Cell<bool>, pub direct_scanout_enabled: Cell<bool>,
pub persistent_output_states: CopyHashMap<Rc<OutputId>, Rc<PersistentOutputState>>, pub persistent_output_states: CopyHashMap<Rc<OutputId>, Rc<PersistentOutputState>>,
@ -292,6 +299,9 @@ pub struct State {
pub supports_presentation_feedback: Cell<bool>, pub supports_presentation_feedback: Cell<bool>,
pub eventfd_cache: Rc<EventfdCache>, pub eventfd_cache: Rc<EventfdCache>,
pub lazy_event_sources: Rc<LazyEventSources>, 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 { // impl Drop for State {
@ -340,37 +350,39 @@ pub struct IdleState {
} }
impl 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.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.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.timeout_changed.set(true);
self.change.trigger(); 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.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.remove(&inhibitor.inhibit_id);
self.inhibitors_changed(); self.inhibitors_changed(state);
if self.inhibitors.is_empty() { if self.inhibitors.is_empty() {
self.resume_inhibited_notifications(); self.resume_inhibited_notifications();
} }
} }
fn inhibitors_changed(&self) { fn inhibitors_changed(&self, state: &State) {
self.inhibitors_changed.set(true); self.inhibitors_changed.set(true);
self.change.trigger(); self.change.trigger();
state.trigger_cci(CCI_IDLE);
} }
fn resume_inhibited_notifications(&self) { fn resume_inhibited_notifications(&self) {
@ -482,30 +494,39 @@ impl ConnectorData {
return; return;
} }
*self.state.borrow_mut() = s.clone(); *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); 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); 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 self.head_managers
.handle_non_desktop_override_changed(s.non_desktop_override); .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); 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); 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); 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 self.head_managers
.handle_colors_change(s.color_space, s.eotf); .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); self.head_managers.handle_mode_change(s.mode);
for head in self.wlr_output_heads.lock().values() { for head in self.wlr_output_heads.lock().values() {
head.handle_mode_change(s.mode); head.handle_mode_change(s.mode);
@ -528,12 +549,14 @@ impl DrmDevData {
self.dev.clone().make_render_device(); 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); 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); 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>>) { pub fn set_render_ctx(&self, ctx: Option<Rc<dyn GfxContext>>) {
self.egg_state.clear();
self.explicit_sync_supported.set(false); self.explicit_sync_supported.set(false);
self.render_ctx.set(ctx.clone()); self.render_ctx.set(ctx.clone());
self.render_ctx_version.fetch_add(1); self.render_ctx_version.fetch_add(1);
@ -756,6 +780,7 @@ impl State {
} }
self.expose_new_singletons(); self.expose_new_singletons();
self.trigger_cci(CCI_COLOR_MANAGEMENT | CCI_GPUS);
} }
fn reload_cursors(&self) { fn reload_cursors(&self) {
@ -1000,6 +1025,7 @@ impl State {
} else { } else {
self.stop_xwayland(); self.stop_xwayland();
} }
self.trigger_cci(CCI_XWAYLAND);
} }
pub fn set_xwayland_use_wire_scale(&self, use_wire_scale: bool) { pub fn set_xwayland_use_wire_scale(&self, use_wire_scale: bool) {
@ -1007,6 +1033,7 @@ impl State {
return; return;
} }
self.update_xwayland_wire_scale(); self.update_xwayland_wire_scale();
self.trigger_cci(CCI_XWAYLAND);
} }
pub fn next_serial(&self, client: Option<&Client>) -> u64 { pub fn next_serial(&self, client: Option<&Client>) -> u64 {
@ -1154,6 +1181,9 @@ impl State {
self.wait_for_syncobj.clear(); self.wait_for_syncobj.clear();
self.xdg_surface_configure_events.clear(); self.xdg_surface_configure_events.clear();
self.lazy_event_sources.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) { pub fn remove_toplevel_id(&self, id: ToplevelIdentifier) {
@ -1703,11 +1733,13 @@ impl State {
pub fn set_color_management_enabled(&self, enabled: bool) { pub fn set_color_management_enabled(&self, enabled: bool) {
self.color_management_enabled.set(enabled); self.color_management_enabled.set(enabled);
self.expose_new_singletons(); self.expose_new_singletons();
self.trigger_cci(CCI_COLOR_MANAGEMENT);
} }
pub fn set_primary_selection_enabled(&self, enabled: bool) { pub fn set_primary_selection_enabled(&self, enabled: bool) {
self.enable_primary_selection.set(enabled); self.enable_primary_selection.set(enabled);
self.expose_new_singletons(); self.expose_new_singletons();
self.trigger_cci(CCI_LOOK_AND_FEEL);
} }
pub fn set_explicit_sync_enabled(&self, enabled: bool) { pub fn set_explicit_sync_enabled(&self, enabled: bool) {
@ -1718,6 +1750,7 @@ impl State {
pub fn set_log_level(&self, level: LogLevel) { pub fn set_log_level(&self, level: LogLevel) {
if let Some(logger) = &self.logger { if let Some(logger) = &self.logger {
logger.set_level(level); logger.set_level(level);
self.trigger_cci(CCI_COMPOSITOR);
} }
} }
@ -1742,6 +1775,7 @@ impl State {
self.root.clone().node_visit(&mut V); self.root.clone().node_visit(&mut V);
self.damage(self.root.extents.get()); self.damage(self.root.extents.get());
self.icons.clear(); self.icons.clear();
self.trigger_cci(CCI_LOOK_AND_FEEL);
} }
pub fn reset_colors(&self) { pub fn reset_colors(&self) {
@ -1776,6 +1810,7 @@ impl State {
pub fn set_ei_socket_enabled(self: &Rc<Self>, enabled: bool) { pub fn set_ei_socket_enabled(self: &Rc<Self>, enabled: bool) {
self.enable_ei_acceptor.set(enabled); self.enable_ei_acceptor.set(enabled);
self.update_ei_acceptor(); self.update_ei_acceptor();
self.trigger_cci(CCI_COMPOSITOR);
} }
pub fn set_workspace_display_order(&self, order: WorkspaceDisplayOrder) { pub fn set_workspace_display_order(&self, order: WorkspaceDisplayOrder) {
@ -1783,6 +1818,7 @@ impl State {
for output in self.root.outputs.lock().values() { for output in self.root.outputs.lock().values() {
output.handle_workspace_display_order_update(); output.handle_workspace_display_order_update();
} }
self.trigger_cci(CCI_COMPOSITOR);
} }
fn spaces_changed(&self) { fn spaces_changed(&self) {
@ -1804,6 +1840,7 @@ impl State {
self.root.clone().node_visit(&mut V); self.root.clone().node_visit(&mut V);
self.damage(self.root.extents.get()); self.damage(self.root.extents.get());
self.icons.update_sizes(self); self.icons.update_sizes(self);
self.trigger_cci(CCI_LOOK_AND_FEEL);
} }
pub fn set_show_bar(&self, show: bool) { pub fn set_show_bar(&self, show: bool) {
@ -1818,15 +1855,18 @@ impl State {
pub fn set_ui_drag_enabled(&self, enabled: bool) { pub fn set_ui_drag_enabled(&self, enabled: bool) {
self.ui_drag_enabled.set(enabled); self.ui_drag_enabled.set(enabled);
self.trigger_cci(CCI_LOOK_AND_FEEL);
} }
pub fn set_ui_drag_threshold(&self, threshold: i32) { pub fn set_ui_drag_threshold(&self, threshold: i32) {
self.ui_drag_threshold_squared self.ui_drag_threshold_squared
.set(threshold.saturating_mul(threshold)); .set(threshold.saturating_mul(threshold));
self.trigger_cci(CCI_LOOK_AND_FEEL);
} }
pub fn set_show_pin_icon(&self, show: bool) { pub fn set_show_pin_icon(&self, show: bool) {
self.show_pin_icon.set(show); self.show_pin_icon.set(show);
self.trigger_cci(CCI_LOOK_AND_FEEL);
for stacked in self.root.stacked.iter() { for stacked in self.root.stacked.iter() {
if let Some(float) = stacked.deref().clone().node_into_float() { if let Some(float) = stacked.deref().clone().node_into_float() {
float.schedule_render_titles(); float.schedule_render_titles();
@ -1836,6 +1876,7 @@ impl State {
pub fn set_float_above_fullscreen(&self, v: bool) { pub fn set_float_above_fullscreen(&self, v: bool) {
self.float_above_fullscreen.set(v); self.float_above_fullscreen.set(v);
self.trigger_cci(CCI_LOOK_AND_FEEL);
for seat in self.globals.seats.lock().values() { for seat in self.globals.seats.lock().values() {
seat.emulate_cursor_moved(); seat.emulate_cursor_moved();
seat.trigger_tree_changed(false); seat.trigger_tree_changed(false);
@ -1849,6 +1890,7 @@ impl State {
} }
fn fonts_changed(&self) { fn fonts_changed(&self) {
self.trigger_cci(CCI_LOOK_AND_FEEL);
struct V; struct V;
impl NodeVisitorBase for V { impl NodeVisitorBase for V {
fn visit_container(&mut self, node: &Rc<ContainerNode>) { fn visit_container(&mut self, node: &Rc<ContainerNode>) {
@ -1872,6 +1914,7 @@ impl State {
theme.font.set(self.theme.default_font.clone()); theme.font.set(self.theme.default_font.clone());
theme.bar_font.set(None); theme.bar_font.set(None);
theme.title_font.set(None); theme.title_font.set(None);
self.egg_state.reset_fonts();
self.fonts_changed(); self.fonts_changed();
} }
@ -1892,6 +1935,16 @@ impl State {
self.fonts_changed(); 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) { pub fn set_bar_position(&self, p: BarPosition) {
self.theme.bar_position.set(p); self.theme.bar_position.set(p);
self.spaces_changed(); self.spaces_changed();

View file

@ -4,6 +4,7 @@ use {
BackendConnectorState, BackendConnectorStateSerial, Connector, ConnectorEvent, BackendConnectorState, BackendConnectorStateSerial, Connector, ConnectorEvent,
ConnectorId, MonitorInfo, ConnectorId, MonitorInfo,
}, },
control_center::CCI_OUTPUTS,
format::XRGB8888, format::XRGB8888,
globals::GlobalName, globals::GlobalName,
ifs::{ ifs::{
@ -108,6 +109,7 @@ pub fn handle(state: &Rc<State>, connector: &Rc<dyn Connector>) {
for mgr in state.head_managers.lock().values() { for mgr in state.head_managers.lock().values() {
mgr.announce(&data); mgr.announce(&data);
} }
state.trigger_cci(CCI_OUTPUTS);
if state.connectors.set(id, data).is_some() { if state.connectors.set(id, data).is_some() {
panic!("Connector id has been reused"); panic!("Connector id has been reused");
} }
@ -147,6 +149,7 @@ impl ConnectorHandler {
self.data.handler.set(None); self.data.handler.set(None);
self.state.connectors.remove(&self.id); self.state.connectors.remove(&self.id);
self.data.head_managers.handle_removed(); self.data.head_managers.handle_removed();
self.state.trigger_cci(CCI_OUTPUTS);
} }
async fn handle_connected(&self, info: MonitorInfo) { async fn handle_connected(&self, info: MonitorInfo) {
@ -162,6 +165,7 @@ impl ConnectorHandler {
} }
self.data.connected.set(false); self.data.connected.set(false);
self.data.head_managers.handle_output_disconnected(); self.data.head_managers.handle_output_disconnected();
self.state.trigger_cci(CCI_OUTPUTS);
for head in self.data.wlr_output_heads.lock().drain_values() { for head in self.data.wlr_output_heads.lock().drain_values() {
head.handle_disconnected(); head.handle_disconnected();
} }
@ -213,12 +217,7 @@ impl ConnectorHandler {
info.primaries, info.primaries,
info.luminance, info.luminance,
)); ));
let schedule = Rc::new(OutputSchedule::new( let schedule = Rc::new(OutputSchedule::new(&self.state, &self.data, &desired_state));
&self.state.ring,
&self.state.eng,
&self.data,
&desired_state,
));
let _schedule = self let _schedule = self
.state .state
.eng .eng
@ -341,6 +340,7 @@ impl ConnectorHandler {
self.data self.data
.head_managers .head_managers
.handle_output_connected(&output_data); .handle_output_connected(&output_data);
self.state.trigger_cci(CCI_OUTPUTS);
self.state.wlr_output_managers.announce_head(&output_data); self.state.wlr_output_managers.announce_head(&output_data);
'outer: loop { 'outer: loop {
while let Some(event) = self.data.connector.event() { while let Some(event) = self.data.connector.event() {
@ -353,6 +353,7 @@ impl ConnectorHandler {
} }
ConnectorEvent::FormatsChanged(formats) => { ConnectorEvent::FormatsChanged(formats) => {
self.data.head_managers.handle_formats_change(&formats); self.data.head_managers.handle_formats_change(&formats);
self.state.trigger_cci(CCI_OUTPUTS);
on.global.formats.set(formats); on.global.formats.set(formats);
} }
ConnectorEvent::State(state) => { ConnectorEvent::State(state) => {
@ -466,6 +467,7 @@ impl ConnectorHandler {
self.data self.data
.head_managers .head_managers
.handle_output_connected(&output_data); .handle_output_connected(&output_data);
self.state.trigger_cci(CCI_OUTPUTS);
self.state.wlr_output_managers.announce_head(&output_data); self.state.wlr_output_managers.announce_head(&output_data);
'outer: loop { 'outer: loop {
while let Some(event) = self.data.connector.event() { 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() { for seat in self.state.globals.seats.lock().values() {
if seat.seat_name() == DEFAULT_SEAT_NAME { if seat.seat_name() == DEFAULT_SEAT_NAME {
self.data.set_seat(Some(seat.clone())); self.data.set_seat(&self.state, Some(seat.clone()));
break; break;
} }
} }
@ -102,6 +102,6 @@ impl DeviceHandler {
.input_device_handlers .input_device_handlers
.borrow_mut() .borrow_mut()
.remove(&self.dev.id()); .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, self_id: s.registry,
name: s.jay_compositor.0, name: s.jay_compositor.0,
interface: JayCompositor.name(), interface: JayCompositor.name(),
version: s.jay_compositor.1.min(27), version: s.jay_compositor.1.min(28),
id: id.into(), id: id.into(),
}); });
self.jay_compositor.set(Some(id)); self.jay_compositor.set(Some(id));

View file

@ -6,6 +6,7 @@ use {
}, },
client::ClientId, client::ClientId,
cmm::cmm_description::ColorDescription, cmm::cmm_description::ColorDescription,
control_center::CCI_OUTPUTS,
cursor::KnownCursor, cursor::KnownCursor,
fixed::Fixed, fixed::Fixed,
gfx_api::{AcquireSync, BufferResv, GfxTexture, ReleaseSync}, gfx_api::{AcquireSync, BufferResv, GfxTexture, ReleaseSync},
@ -243,6 +244,7 @@ impl OutputNode {
.connector .connector
.head_managers .head_managers
.handle_tearing_active_change(tearing); .handle_tearing_active_change(tearing);
self.state.trigger_cci(CCI_OUTPUTS);
} }
} }
@ -501,6 +503,7 @@ impl OutputNode {
.connector .connector
.head_managers .head_managers
.handle_scale_change(scale); .handle_scale_change(scale);
self.state.trigger_cci(CCI_OUTPUTS);
for head in self.global.connector.wlr_output_heads.lock().values() { for head in self.global.connector.wlr_output_heads.lock().values() {
head.handle_new_scale(scale); head.handle_new_scale(scale);
} }
@ -873,6 +876,7 @@ impl OutputNode {
.connector .connector
.head_managers .head_managers
.handle_transform_change(transform); .handle_transform_change(transform);
self.state.trigger_cci(CCI_OUTPUTS);
for head in self.global.connector.wlr_output_heads.lock().values() { for head in self.global.connector.wlr_output_heads.lock().values() {
head.hande_transform_change(transform); head.hande_transform_change(transform);
} }
@ -935,6 +939,7 @@ impl OutputNode {
.connector .connector
.head_managers .head_managers
.handle_position_size_change(self); .handle_position_size_change(self);
self.state.trigger_cci(CCI_OUTPUTS);
} }
pub fn update_state(self: &Rc<Self>, old: BackendConnectorState, state: BackendConnectorState) { pub fn update_state(self: &Rc<Self>, old: BackendConnectorState, state: BackendConnectorState) {
@ -989,6 +994,7 @@ impl OutputNode {
.connector .connector
.head_managers .head_managers
.handle_brightness_change(brightness); .handle_brightness_change(brightness);
self.state.trigger_cci(CCI_OUTPUTS);
} }
} }
@ -1004,6 +1010,7 @@ impl OutputNode {
.connector .connector
.head_managers .head_managers
.handle_use_native_gamut_change(use_native_gamut); .handle_use_native_gamut_change(use_native_gamut);
self.state.trigger_cci(CCI_OUTPUTS);
} }
} }
@ -1015,6 +1022,7 @@ impl OutputNode {
.connector .connector
.head_managers .head_managers
.handle_blend_space_change(blend_space); .handle_blend_space_change(blend_space);
self.state.trigger_cci(CCI_OUTPUTS);
} }
} }
fn find_stacked_at( fn find_stacked_at(
@ -1480,6 +1488,7 @@ impl OutputNode {
.connector .connector
.head_managers .head_managers
.handle_vrr_mode_change(mode); .handle_vrr_mode_change(mode);
self.state.trigger_cci(CCI_OUTPUTS);
for head in self.global.connector.wlr_output_heads.lock().values() { for head in self.global.connector.wlr_output_heads.lock().values() {
head.handle_vrr_mode_change(mode); head.handle_vrr_mode_change(mode);
} }
@ -1494,6 +1503,7 @@ impl OutputNode {
.connector .connector
.head_managers .head_managers
.handle_tearing_mode_change(mode); .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) { pub fn set_flip_margin(&self, margin_ns: u64) {
self.flip_margin_ns.set(Some(margin_ns)); 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() parent.node_is_workspace()
} }
#[expect(dead_code)]
pub fn property_changed_source(&self) -> &Rc<LazyEventSource> { pub fn property_changed_source(&self) -> &Rc<LazyEventSource> {
self.property_changed_source self.property_changed_source
.get_or_init(|| self.state.lazy_event_sources.create_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 nonblock;
pub mod num_cpus; pub mod num_cpus;
pub mod numcell; pub mod numcell;
pub mod object_drop_queue;
pub mod on_change; pub mod on_change;
pub mod on_drop_event; pub mod on_drop_event;
pub mod once; pub mod once;

View file

@ -100,6 +100,14 @@ impl<T> NumCell<T> {
{ {
!self.is_zero() !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> { 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, thiserror::Error,
}; };
#[derive(Copy, Clone, Eq, PartialEq, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct Opaque { pub struct Opaque {
lo: u64, lo: u64,
hi: u64, hi: u64,

View file

@ -14,7 +14,6 @@ pub fn pipe() -> Result<Pipe<OwnedFd, OwnedFd>, OsError> {
} }
impl<L, R> Pipe<L, R> { impl<L, R> Pipe<L, R> {
#[expect(dead_code)]
pub fn map_read<Lprime>(self, map: impl FnOnce(L) -> Lprime) -> Pipe<Lprime, R> { pub fn map_read<Lprime>(self, map: impl FnOnce(L) -> Lprime) -> Pipe<Lprime, R> {
Pipe { Pipe {
read: map(self.read), 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> { pub fn map_write<Rprime>(self, map: impl FnOnce(R) -> Rprime) -> Pipe<L, Rprime> {
Pipe { Pipe {
read: self.read, 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); pub struct ToplevelIdentifier(Opaque);
unsafe impl UnsafeCellCloneSafe for ToplevelIdentifier {} unsafe impl UnsafeCellCloneSafe for ToplevelIdentifier {}

View file

@ -1,8 +1,12 @@
use { use {
crate::{ crate::{
format::Format, format::Format,
gfx_api::SyncFile,
utils::{compat::IoctlNumber, oserror::OsError}, utils::{compat::IoctlNumber, oserror::OsError},
video::{LINEAR_MODIFIER, Modifier}, video::{
LINEAR_MODIFIER, Modifier,
drm::{DrmError, syncobj::merge_sync_files},
},
}, },
arrayvec::ArrayVec, arrayvec::ArrayVec,
std::{cell::OnceCell, rc::Rc, sync::OnceLock}, std::{cell::OnceCell, rc::Rc, sync::OnceLock},
@ -113,6 +117,22 @@ impl DmaBuf {
} }
Ok(()) 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 _; const DMA_BUF_BASE: u64 = b'b' as _;

View file

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

View file

@ -18,7 +18,6 @@ pub struct UsrJaySyncFileSurface {
} }
impl UsrJaySyncFileSurface { impl UsrJaySyncFileSurface {
#[expect(dead_code)]
pub fn set_acquire(&self, sf: Option<&FdSync>) { pub fn set_acquire(&self, sf: Option<&FdSync>) {
match sf.and_then(|s| s.get_sync_file()) { match sf.and_then(|s| s.get_sync_file()) {
None => { None => {
@ -33,7 +32,6 @@ impl UsrJaySyncFileSurface {
} }
} }
#[expect(dead_code)]
pub fn get_release(&self) -> Rc<UsrJaySyncFileRelease> { pub fn get_release(&self) -> Rc<UsrJaySyncFileRelease> {
let obj = Rc::new(UsrJaySyncFileRelease { let obj = Rc::new(UsrJaySyncFileRelease {
id: self.con.id(), id: self.con.id(),

View file

@ -21,7 +21,6 @@ pub struct UsrWlDataDevice {
} }
impl UsrWlDataDevice { impl UsrWlDataDevice {
#[expect(dead_code)]
pub fn set_selection(&self, serial: u32, source: &UsrWlDataSource) { pub fn set_selection(&self, serial: u32, source: &UsrWlDataSource) {
self.con.request(SetSelection { self.con.request(SetSelection {
self_id: self.id, self_id: self.id,

View file

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

View file

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

View file

@ -21,7 +21,6 @@ pub trait UsrWlDataSourceOwner {
} }
impl UsrWlDataSource { impl UsrWlDataSource {
#[expect(dead_code)]
pub fn offer(&self, mime_type: &str) { pub fn offer(&self, mime_type: &str) {
self.con.request(Offer { self.con.request(Offer {
self_id: self.id, self_id: self.id,

View file

@ -41,7 +41,6 @@ pub trait UsrWlPointerOwner {
} }
impl UsrWlPointer { impl UsrWlPointer {
#[expect(dead_code)]
pub fn set_cursor(&self, serial: u32, cursor: Option<&UsrWlSurface>, hot_x: i32, hot_y: i32) { pub fn set_cursor(&self, serial: u32, cursor: Option<&UsrWlSurface>, hot_x: i32, hot_y: i32) {
self.con.request(SetCursor { self.con.request(SetCursor {
self_id: self.id, self_id: self.id,

View file

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

View file

@ -15,7 +15,6 @@ pub struct UsrWpCursorShapeDeviceV1 {
} }
impl UsrWpCursorShapeDeviceV1 { impl UsrWpCursorShapeDeviceV1 {
#[expect(dead_code)]
pub fn set_shape(&self, serial: u32, cursor: KnownCursor) { pub fn set_shape(&self, serial: u32, cursor: KnownCursor) {
self.con.request(SetShape { self.con.request(SetShape {
self_id: self.id, self_id: self.id,

View file

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

View file

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

View file

@ -16,7 +16,6 @@ pub struct UsrXdgToplevel {
} }
impl UsrXdgToplevel { impl UsrXdgToplevel {
#[expect(dead_code)]
pub fn set_title(&self, title: &str) { pub fn set_title(&self, title: &str) {
self.con.request(SetTitle { self.con.request(SetTitle {
self_id: self.id, self_id: self.id,
@ -24,7 +23,6 @@ impl UsrXdgToplevel {
}); });
} }
#[expect(dead_code)]
pub fn set_fullscreen(&self, fullscreen: bool) { pub fn set_fullscreen(&self, fullscreen: bool) {
match fullscreen { match fullscreen {
true => { true => {

View file

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

View file

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

View file

@ -5,6 +5,7 @@ use {
crate::{ crate::{
client::{ClientCaps, ClientError}, client::{ClientCaps, ClientError},
compositor::DISPLAY, compositor::DISPLAY,
control_center::CCI_XWAYLAND,
forker::{ForkerError, ForkerProxy}, forker::{ForkerError, ForkerProxy},
ifs::{ ifs::{
ipc::{DataOfferId, DataSourceId, IpcLocation, x_data_offer::XDataOffer}, 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)); let display = Rc::new(format!(":{}", xsocket.id));
forker.setenv(DISPLAY.as_bytes(), display.as_bytes()); forker.setenv(DISPLAY.as_bytes(), display.as_bytes());
state.xwayland.display.set(Some(display.clone())); state.xwayland.display.set(Some(display.clone()));
state.trigger_cci(CCI_XWAYLAND);
let _unsetenv = on_drop(|| { let _unsetenv = on_drop(|| {
forker.unsetenv(DISPLAY.as_bytes()); forker.unsetenv(DISPLAY.as_bytes());
state.xwayland.display.take(); state.xwayland.display.take();
state.trigger_cci(CCI_XWAYLAND);
}); });
log::info!("Allocated display :{} for Xwayland", xsocket.id); log::info!("Allocated display :{} for Xwayland", xsocket.id);
log::info!("Waiting for connection attempt"); log::info!("Waiting for connection attempt");
@ -212,9 +215,11 @@ async fn run(
state.xwayland.queue.clear(); state.xwayland.queue.clear();
state.xwayland.pidfd.set(Some(pidfd.clone())); state.xwayland.pidfd.set(Some(pidfd.clone()));
state.xwayland.client.set(Some(client.clone())); state.xwayland.client.set(Some(client.clone()));
state.trigger_cci(CCI_XWAYLAND);
let _remove_pidfd = on_drop(|| { let _remove_pidfd = on_drop(|| {
state.xwayland.pidfd.take(); state.xwayland.pidfd.take();
state.xwayland.client.take(); state.xwayland.client.take();
state.trigger_cci(CCI_XWAYLAND);
}); });
{ {
let shared = Rc::new(XwmShared::default()); let shared = Rc::new(XwmShared::default());

View file

@ -90,6 +90,7 @@ pub enum SimpleCommand {
ToggleSimpleImEnabled, ToggleSimpleImEnabled,
ReloadSimpleIm, ReloadSimpleIm,
EnableUnicodeInput, EnableUnicodeInput,
OpenControlCenter,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -211,6 +212,12 @@ pub struct Theme {
pub bar_separator_width: Option<i32>, 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)] #[derive(Debug, Clone)]
pub struct Status { pub struct Status {
pub format: MessageFormat, pub format: MessageFormat,
@ -510,6 +517,7 @@ pub struct Config {
pub auto_reload: Option<bool>, pub auto_reload: Option<bool>,
pub log_level: Option<LogLevel>, pub log_level: Option<LogLevel>,
pub theme: Theme, pub theme: Theme,
pub egui: Egui,
pub gfx_api: Option<GfxApi>, pub gfx_api: Option<GfxApi>,
pub direct_scanout_enabled: Option<bool>, pub direct_scanout_enabled: Option<bool>,
pub drm_devices: Vec<ConfigDrmDevice>, pub drm_devices: Vec<ConfigDrmDevice>,

View file

@ -19,6 +19,7 @@ mod connector_match;
mod content_type; mod content_type;
mod drm_device; mod drm_device;
mod drm_device_match; mod drm_device_match;
mod egui;
mod env; mod env;
pub mod exec; pub mod exec;
mod fallback_output_mode; mod fallback_output_mode;

View file

@ -167,6 +167,7 @@ impl ActionParser<'_> {
"toggle-simple-im-enabled" => ToggleSimpleImEnabled, "toggle-simple-im-enabled" => ToggleSimpleImEnabled,
"reload-simple-im" => ReloadSimpleIm, "reload-simple-im" => ReloadSimpleIm,
"enable-unicode-input" => EnableUnicodeInput, "enable-unicode-input" => EnableUnicodeInput,
"open-control-center" => OpenControlCenter,
_ => { _ => {
return Err( return Err(
ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span) ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span)

View file

@ -1,7 +1,7 @@
use { use {
crate::{ crate::{
config::{ config::{
Action, Config, Libei, Theme, UiDrag, Action, Config, Egui, Libei, Theme, UiDrag,
context::Context, context::Context,
extractor::{Extractor, ExtractorError, arr, bol, int, opt, recover, str, val}, extractor::{Extractor, ExtractorError, arr, bol, int, opt, recover, str, val},
parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parser::{DataType, ParseResult, Parser, UnexpectedDataType},
@ -13,6 +13,7 @@ use {
connector::ConnectorsParser, connector::ConnectorsParser,
drm_device::DrmDevicesParser, drm_device::DrmDevicesParser,
drm_device_match::DrmDeviceMatchParser, drm_device_match::DrmDeviceMatchParser,
egui::EguiParser,
env::EnvParser, env::EnvParser,
fallback_output_mode::FallbackOutputModeParser, fallback_output_mode::FallbackOutputModeParser,
float::FloatParser, float::FloatParser,
@ -150,6 +151,7 @@ impl Parser for ConfigParser<'_> {
simple_im_val, simple_im_val,
show_titles, show_titles,
fallback_output_mode_val, fallback_output_mode_val,
egui_val,
), ),
) = ext.extract(( ) = ext.extract((
( (
@ -208,6 +210,7 @@ impl Parser for ConfigParser<'_> {
opt(val("simple-im")), opt(val("simple-im")),
recover(opt(bol("show-titles"))), recover(opt(bol("show-titles"))),
opt(val("fallback-output-mode")), opt(val("fallback-output-mode")),
opt(val("egui")),
), ),
))?; ))?;
let mut keymap = None; 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; let mut gfx_api = None;
if let Some(value) = gfx_api_val { if let Some(value) = gfx_api_val {
match value.parse(&mut GfxApiParser) { match value.parse(&mut GfxApiParser) {
@ -556,6 +568,7 @@ impl Parser for ConfigParser<'_> {
auto_reload: auto_reload.despan(), auto_reload: auto_reload.despan(),
log_level, log_level,
theme, theme,
egui,
gfx_api, gfx_api,
drm_devices, drm_devices,
direct_scanout_enabled: direct_scanout.despan(), 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-u = "toggle-fullscreen"
alt-f = "focus-parent" alt-f = "focus-parent"
alt-c = "open-control-center"
alt-shift-c = "close" alt-shift-c = "close"
alt-shift-f = "toggle-floating" alt-shift-f = "toggle-floating"
Super_L = { type = "exec", exec = "alacritty" } Super_L = { type = "exec", exec = "alacritty" }

View file

@ -36,16 +36,17 @@ use {
is_reload, is_reload,
keyboard::Keymap, keyboard::Keymap,
logging::set_log_level, logging::set_log_level,
on_devices_enumerated, on_idle, on_unload, quit, reload, set_color_management_enabled, on_devices_enumerated, on_idle, on_unload, open_control_center, quit, reload,
set_default_workspace_capture, set_explicit_sync_enabled, set_float_above_fullscreen, set_color_management_enabled, set_default_workspace_capture, set_explicit_sync_enabled,
set_idle, set_idle_grace_period, set_middle_click_paste_enabled, set_show_bar, set_float_above_fullscreen, set_idle, set_idle_grace_period,
set_show_float_pin_icon, set_show_titles, set_ui_drag_enabled, set_ui_drag_threshold, 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}, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command},
switch_to_vt, switch_to_vt,
tasks::{self, JoinHandle}, tasks::{self, JoinHandle},
theme::{ theme::{
reset_colors, reset_font, reset_sizes, set_bar_font, set_bar_position, set_font, reset_colors, reset_font, reset_sizes, set_bar_font, set_bar_position,
set_title_font, set_egui_monospace_fonts, set_egui_proportional_fonts, set_font, set_title_font,
}, },
toggle_float_above_fullscreen, toggle_show_bar, toggle_show_titles, toggle_float_above_fullscreen, toggle_show_bar, toggle_show_titles,
video::{ video::{
@ -245,6 +246,7 @@ impl Action {
let persistent = state.persistent.clone(); let persistent = state.persistent.clone();
b.new(move || persistent.seat.enable_unicode_input()) b.new(move || persistent.seat.enable_unicode_input())
} }
SimpleCommand::OpenControlCenter => b.new(open_control_center),
}, },
Action::Multi { actions } => { Action::Multi { actions } => {
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); 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 { if let Some(v) = config.fallback_output_mode {
persistent.seat.set_fallback_output_mode(v); 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 { fn create_command(exec: &Exec) -> Command {

View file

@ -1081,6 +1081,10 @@
"fallback-output-mode": { "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", "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" "$ref": "#/$defs/FallbackOutputMode"
},
"egui": {
"description": "Sets the egui settings of the compositor.\n",
"$ref": "#/$defs/Egui"
} }
}, },
"required": [] "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": { "Eotf": {
"type": "string", "type": "string",
"description": "The EOTF of an output.\n", "description": "The EOTF of an output.\n",
@ -1901,7 +1928,8 @@
"disable-simple-im", "disable-simple-im",
"toggle-simple-im-enabled", "toggle-simple-im-enabled",
"reload-simple-im", "reload-simple-im",
"enable-unicode-input" "enable-unicode-input",
"open-control-center"
] ]
}, },
"SimpleIm": { "SimpleIm": {

View file

@ -2248,6 +2248,12 @@ The table has the following fields:
The value of this field should be a [FallbackOutputMode](#types-FallbackOutputMode). 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> <a name="types-Connector"></a>
### `Connector` ### `Connector`
@ -2588,6 +2594,32 @@ The table has the following fields:
The numbers should be integers. 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> <a name="types-Eotf"></a>
### `Eotf` ### `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. This has no effect if the simple IM is not currently active.
- `open-control-center`:
Opens the control center.
<a name="types-SimpleIm"></a> <a name="types-SimpleIm"></a>

View file

@ -1089,6 +1089,8 @@ SimpleActionName:
Enables Unicode input in the simple, XCompose based input method. Enables Unicode input in the simple, XCompose based input method.
This has no effect if the simple IM is not currently active. This has no effect if the simple IM is not currently active.
- value: open-control-center
description: Opens the control center.
Color: Color:
@ -3004,6 +3006,11 @@ Config:
```toml ```toml
fallback-output-mode = "focus" fallback-output-mode = "focus"
``` ```
egui:
ref: Egui
required: false
description: |
Sets the egui settings of the compositor.
Idle: Idle:
@ -4426,3 +4433,28 @@ FallbackOutputMode:
description: Use the output the cursor is on. description: Use the output the cursor is on.
- value: focus - value: focus
description: Use the output the focus is on (highlighted window). 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 # events
event client_id { event client_id {

View file

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