mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
Compare commits
1040 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ba57fcf25 | |||
| 126ca37d96 | |||
| e6bd60fbb1 | |||
| a605a3f016 | |||
| ef44adea69 | |||
| 7fdb918cd0 | |||
| 8347cc20dc | |||
| 51a176ec4a | |||
| d618daf6b9 | |||
| 357f9157cc | |||
| c4a759e620 | |||
| f369a0f810 | |||
| 71251a7003 | |||
| 2415346caa | |||
| 3f2b7e63ba | |||
| ae89cb6017 | |||
| b6fc4d0455 | |||
| d8265ad34e | |||
| 3b864dc104 | |||
| 15093221ed | |||
| ac7b3fbf19 | |||
| bb8eb377c7 | |||
| 6169c0312a | |||
| 2ae99224ab | |||
| 4f63e13385 | |||
| 46a8f81160 | |||
| 0d6843ea67 | |||
| 6d083ea497 | |||
| 7a42140d6c | |||
| eeb411bef5 | |||
| defd4c5c4d | |||
| 7227e64149 | |||
| c98537a2b0 | |||
| 9c103f1f1d | |||
| 2aff1ec71a | |||
| 3466fc0a66 | |||
| f917932b3e | |||
| 89b7423ee5 | |||
| a2efaf2816 | |||
| 5816691460 | |||
| 4b5e9e6cb0 | |||
| a8259b4cea | |||
| 9d3d7cb0e9 | |||
| 398bc78ea0 | |||
| caa6189448 | |||
| 86f57c2ec7 | |||
| 3cc67897af | |||
| a99489c6c0 | |||
| 0763c7e196 | |||
| fb5c5204e8 | |||
| d207cd385b | |||
| 99bf2df2b4 | |||
| 09be90f4e6 | |||
| dfc42b9d82 | |||
| e2b9838d89 | |||
| 816a0d479c | |||
| 84323d10a4 | |||
| b956f2775c | |||
| 9ff2f83db0 | |||
| 7a10f71ee5 | |||
| ea7add3563 | |||
| e9c6f08906 | |||
| 17343a6740 | |||
| 140d726cd3 | |||
| c37d3b3442 | |||
| 497f186422 | |||
| 3e31c134a6 | |||
| fe682938db | |||
| 6142922ca4 | |||
| 4b44fba14c | |||
| 57639ca84c | |||
| ec88aae77d | |||
| 6c9705dd4b | |||
| eb590c5346 | |||
| 02baad91ac | |||
| 68589cd5a1 | |||
| f2c690802b | |||
| 9d6037b94c | |||
| 7b4cf094ef | |||
| 446bc155ce | |||
| 3289324ce4 | |||
| 9fb02b9571 | |||
| 0e9496b01e | |||
| 82dabc21f3 | |||
| 39b3d62873 | |||
| af080a03cd | |||
| 5f117c61dc | |||
| cb857e32e4 | |||
| 199be26947 | |||
| d5c0c74d2c | |||
| 9bb292ec82 | |||
| a1ba6bcaa0 | |||
| fd389af6d8 | |||
| db09727b18 | |||
| c9d6478c3c | |||
| 758cca5432 | |||
| 78e3daf5f8 | |||
| a99a0b2492 | |||
| bfd42c74f4 | |||
| 501ea47128 | |||
| d2a1cf53b4 | |||
| 62d47d77d5 | |||
| 85cd64e830 | |||
| 55c14eebf2 | |||
| 3fe67549b4 | |||
| 1835b532d9 | |||
| e6d82d3ee3 | |||
| fae3a27641 | |||
| 31e76cf451 | |||
| b8a9be542f | |||
| 59de6918b3 | |||
| bd3d554389 | |||
| af1fca35bb | |||
| 9571d149b2 | |||
| 99358e36b3 | |||
| 8b878f355f | |||
| 395b6d9a4f | |||
| 25f24f668c | |||
| 929eaf0d69 | |||
| ce3103949f | |||
| ef60dd81d7 | |||
| 7671a5d833 | |||
| 3f09352067 | |||
| 5059cce886 | |||
| b20dd226c0 | |||
| acb69c3b4d | |||
| dbe0a9e293 | |||
| d3a79faeec | |||
| 21630ddb5e | |||
| 9e5e0c85bb | |||
| 5cd8040d1a | |||
| 86351938f2 | |||
| ee4c5e23ab | |||
| ffd6acc0aa | |||
| cee11dc329 | |||
| 59a42249a4 | |||
| 74b016202b | |||
| 6ab055a4b9 | |||
| 98bd9b7abb | |||
| f36e1c2ef2 | |||
| 2243615fe9 | |||
| 7884d3bfea | |||
| fdbc485d78 | |||
| 7e253d2687 | |||
| 15ba2ab300 | |||
| 37840a418a | |||
| 4a4c972ffb | |||
| ba933773ab | |||
| f1cca1a6ca | |||
| 763cd564e3 | |||
| 95eafba346 | |||
| df94662435 | |||
| 430b155929 | |||
| c359d24825 | |||
| e8da89a430 | |||
| feae8c15e6 | |||
| b49f7dcb4d | |||
| 60034a57ef | |||
| 2adbf33fb6 | |||
| 28cc84fbd1 | |||
| e10b968eb0 | |||
| 3b1bf34e21 | |||
| bd927b54e0 | |||
| 66d3a3bd82 | |||
| 36489f1daa | |||
| b2c34e7fe9 | |||
| dcc291d701 | |||
| 8d43efe4ac | |||
| 3bb7e60311 | |||
| d639eb0032 | |||
| d91499486e | |||
| f7106f9658 | |||
| 835490c59a | |||
| 5cde00f6c6 | |||
| 7dc015e16b | |||
| 0db48e2f1b | |||
| 7cfecf4b1b | |||
| 3142838e9e | |||
| 4534d37266 | |||
| ec5112d779 | |||
| c709696237 | |||
| b271409509 | |||
| 500dcca9b7 | |||
| 7210045b2a | |||
| ed20822ce9 | |||
| e88dfae46f | |||
| f95d5a82df | |||
| 7f72c358d5 | |||
| 0d4f0f00c0 | |||
| f2663c738c | |||
| c3609efb7a | |||
| fd1f43673c | |||
| 9d10def7e8 | |||
| e251ca7340 | |||
| 9a527cc571 | |||
| 39f52b7585 | |||
| b447b1f4de | |||
| 1a0fab05b6 | |||
| fbb399f01d | |||
| 6a80ec4704 | |||
| e8b158641b | |||
| 27a715aded | |||
| 926e63a5f3 | |||
| e879199880 | |||
| 5b6b6a5fe1 | |||
| e11af089aa | |||
| 5e549e1323 | |||
| 287480b541 | |||
| a022fedd51 | |||
| a4b8e100c0 | |||
| 62576796be | |||
| 31891e6642 | |||
| 392fc27de1 | |||
| 9e560e7e60 | |||
| cee2ec7ab7 | |||
| 8c4ebb00a1 | |||
| f6aa8c1793 | |||
| a5d58d670b | |||
| b4922086ce | |||
| fd3b1f2b6c | |||
| ee0e2c7f1b | |||
| 4f16be9e4d | |||
| 0f30306fe5 | |||
| 1c6037e612 | |||
| fed86fdb5d | |||
| 3e21585861 | |||
| 9f9c4a99af | |||
| b220cdbe7e | |||
| df219b5134 | |||
| 8cdabe8adf | |||
| 8737067af5 | |||
| 50a99f6356 | |||
| 993c5ce8af | |||
| 47dd338340 | |||
| 87b6c12625 | |||
| b351f6ff22 | |||
| 12817a682d | |||
| 88614c08fe | |||
| 4f5c8e745b | |||
| f30413a744 | |||
| 3b8ce12316 | |||
| 880386e563 | |||
| 266c6c3878 | |||
| 7b033aa7c6 | |||
| efd8372b20 | |||
| 74a30be10b | |||
| 1c521e4831 | |||
| eda43b2b93 | |||
| 593241d2f0 | |||
| 69627bdc64 | |||
| 3fa373c720 | |||
| 083a56c729 | |||
| 88fcf0c2a9 | |||
| 26618f8d50 | |||
| 9f205d465c | |||
| d6e736aaf0 | |||
| 36b28d9b96 | |||
| 66113d7d76 | |||
| aa2e8b402c | |||
| 311f3be5d8 | |||
| 70dcd229cf | |||
| 26fe4a489a | |||
| 2363cf48e7 | |||
| 848294c09b | |||
| 693d935538 | |||
| 16405b9b2b | |||
| 4719cc6d59 | |||
| 98b92d4db7 | |||
| 1bdded7a44 | |||
| 9bfe90dee1 | |||
| c153349c62 | |||
| 5b6b5536fd | |||
| bac22dfe9f | |||
| bca6545288 | |||
| b94a5db879 | |||
| 4a4dcb85ef | |||
| 7b70cb66bc | |||
| cd6522bcc6 | |||
| 8885233c7e | |||
| 7478784343 | |||
| dca187de37 | |||
| fe660a253b | |||
| ad49e5820a | |||
| 4c40e6ce06 | |||
| 44c9797844 | |||
| 652d2923bb | |||
| 85349ce475 | |||
| 92cc2b89f7 | |||
| 078383ea82 | |||
| d27d6a504d | |||
| ec5144feca | |||
| 05e0e44a77 | |||
| 108e88e211 | |||
| a693f64c41 | |||
| 5c0468d469 | |||
| f2b1fc66f2 | |||
| 22302bf224 | |||
| bb6663ebac | |||
| c6e98d5a96 | |||
| d077350ae4 | |||
| f01c840ebe | |||
| ca1500ae90 | |||
| d7f3ca00c7 | |||
| fd8140e091 | |||
| d94fbe9895 | |||
| 7816f20e6a | |||
| 0d3610416c | |||
| 377ad54016 | |||
| 9e794f358b | |||
| 4e17cbb9ea | |||
| 4c98b87486 | |||
| 5b753be213 | |||
| a605e7f622 | |||
| 513488f6b8 | |||
| 43ea4a172a | |||
| d47b59879a | |||
| ef80bcc834 | |||
| eb8bd3894a | |||
| 7e552333a9 | |||
| 213eafa203 | |||
| 7b18ff8870 | |||
| 5246e2ff25 | |||
| dde9214ae4 | |||
| 29b7a41692 | |||
| 216753678a | |||
| b9e67f6565 | |||
| 3a481b5250 | |||
| 20769b4c2f | |||
| 14ac2cff4c | |||
| fde627d955 | |||
| 6942ecc13a | |||
| 963ff14ed0 | |||
| 96a3ded2ec | |||
| a21196ec54 | |||
| 0b83d9932b | |||
| 6bd92ab926 | |||
| 02eccf7762 | |||
| 89cf276779 | |||
| bc701cd529 | |||
| bfd81fc290 | |||
| 0dd8e883b0 | |||
| c31b58e2c9 | |||
| b163045757 | |||
| 41e9ec1364 | |||
| 64544a5726 | |||
| d7d5a7f8f6 | |||
| a451f75917 | |||
| 1515410012 | |||
| 8f9e0d029c | |||
| 90f24da631 | |||
| df70140b36 | |||
| f90eb0cbe4 | |||
| 55e2ea0c3b | |||
| 1d883931b4 | |||
| b65fad09d8 | |||
| 09a559d3c9 | |||
| 9fc749f3d4 | |||
| f836d1c28a | |||
| 4f05a74aa8 | |||
| c30f522ef2 | |||
| 397e704d64 | |||
| acc9d3e409 | |||
| 0c59fc304c | |||
| abd7f1dce3 | |||
| 1d87da00b7 | |||
| 91515ac6dc | |||
| 7ec771f7ec | |||
| a42a5ac696 | |||
| b31c0359eb | |||
| 934e5a6033 | |||
| 690d635505 | |||
| a444efd0eb | |||
| c41f93a468 | |||
| 900da597e4 | |||
| d320833f40 | |||
| c384b2489f | |||
| ddcac86d1d | |||
| 734e3a6d3c | |||
| f18b1a7043 | |||
| 7d24ad23c2 | |||
| 691bc064bb | |||
| 553b1ba852 | |||
| d5592743cb | |||
| 019e75955d | |||
| 32ad545f84 | |||
| 4eddcef1be | |||
| 68776f1cee | |||
| a0e2a15c60 | |||
| 88c6778771 | |||
| 73f6d3366e | |||
| 48a4d5c8a3 | |||
| 6f2f7fa259 | |||
| 49ddf66c2f | |||
| a169e0335d | |||
| e412a0fc6b | |||
| fb5fedbf24 | |||
| 6b04b1e454 | |||
| 0c340ec5ea | |||
| 34679c75a4 | |||
| 1d3820a064 | |||
| 1c749f578c | |||
| 3a887a6e49 | |||
| beef2da628 | |||
| 9b4d73f13a | |||
| 0226d9aec2 | |||
| 902222675a | |||
| ec43493522 | |||
| baa0518912 | |||
| d665079b84 | |||
| f0d935dee1 | |||
| 314b82caa0 | |||
| 8f79139b78 | |||
| c5296b870a | |||
| 78697d1cea | |||
| 852da5714a | |||
| 4f79303811 | |||
| f294d527e1 | |||
| 54a1cd5069 | |||
| 748d90b443 | |||
| 128b01e049 | |||
| 788c9c6c54 | |||
| a10705fb20 | |||
| b01b8afa8c | |||
| acd4cb51aa | |||
| 5ebcae997e | |||
| 2511a98e8b | |||
| a7692d10c4 | |||
| c892f04c96 | |||
| 3aad5a39ea | |||
| 7f025da5b6 | |||
| 01285bdbbe | |||
| 8182484572 | |||
| 0584dd2f1e | |||
| bd559a2660 | |||
| b4add625b2 | |||
| 890bbff007 | |||
| b853d5b124 | |||
| 693e0e09f7 | |||
| d52356b131 | |||
| b11b995d03 | |||
| 99ba295082 | |||
| 8c2b5957eb | |||
| 4472164447 | |||
| a3cbe3514b | |||
| efa7c862a4 | |||
| 0df7a085de | |||
| 6ae51f287c | |||
| 36076d5279 | |||
| 427c4e3982 | |||
| 1632ce87a5 | |||
| c523c80598 | |||
| 0bd6df507b | |||
| 6e41220dbf | |||
| e05bc269e6 | |||
| d574341f1f | |||
| 481958f8f7 | |||
| 4094469d59 | |||
| 2261fcb631 | |||
| 279c8b6aa2 | |||
| e35c630c1d | |||
| d3047afa7f | |||
| a03783f54c | |||
| cbf0d6190d | |||
| 89c991b636 | |||
| bbbd35e9ef | |||
| c308be315d | |||
| d825e3125e | |||
| 64288de04e | |||
| fb4471e69d | |||
| 8be8694f5f | |||
| 60b78dc2cd | |||
| 80fe5a8167 | |||
| df58c49876 | |||
| 7dee2f6995 | |||
| 623687e59b | |||
| 5958d3be62 | |||
| 142e57450d | |||
| 80815a1591 | |||
| 8412bfb813 | |||
| a0f279691a | |||
| 92aeddb9fe | |||
| d7da88853b | |||
| 89678c7b1e | |||
| 098c826095 | |||
| befbdc3ae5 | |||
| dca0364f4c | |||
| 37771259d9 | |||
| 4618e4851c | |||
| b2ca280c49 | |||
| bf6995f759 | |||
| ab0cce7cb7 | |||
| 2e422fc026 | |||
| a2f9d132a0 | |||
| 1973b97cc2 | |||
| b3c6f0e661 | |||
| 6998b17f9e | |||
| ed9932d70d | |||
| a5f3b2a949 | |||
| 152ed59502 | |||
| 8e16be9e11 | |||
| 300701f44e | |||
| d1370622d8 | |||
| 0134166009 | |||
| ddb9084260 | |||
| 0224452cef | |||
| c17d4dc050 | |||
| 4e33f45522 | |||
| b16d7abb35 | |||
| 2f17a30157 | |||
| 0dbd14ebdc | |||
| 8b3d8ccb47 | |||
| f8ff2e4e28 | |||
| 044f0d41a5 | |||
| 4089bebd83 | |||
| d4787c75fd | |||
| 3bf0a57b82 | |||
| cc505ae49f | |||
| 2f6de136dd | |||
| da21b50137 | |||
| a38a5c529f | |||
| 44b5612697 | |||
| 0113292cf6 | |||
| 4741ab2e04 | |||
| 08fb9435fd | |||
| 793e92e9d6 | |||
| a7c57f4faf | |||
| 8409107a5b | |||
| 9089c3fb02 | |||
| 6c897d5201 | |||
| 6cb5135f34 | |||
| 44bf45794e | |||
| d6da9f47d8 | |||
| be05b66ac3 | |||
| d1998ae3fa | |||
| 3c2e1554c6 | |||
| 744955ba69 | |||
| 7af33f9e6a | |||
| 3c0705b0ae | |||
| 4ea4d2bd3b | |||
| 6c52077d92 | |||
| 73bf7b1730 | |||
| b394cb6379 | |||
| 60854e180e | |||
| 5b4750a009 | |||
| ad50dd21fe | |||
| 8b0cb0bb57 | |||
| a24a6e4e3c | |||
| 6fba4c371e | |||
| 27911431db | |||
| db6447ed79 | |||
| 99c0fabee6 | |||
| fc99724aba | |||
| 88fbc62b1d | |||
| e8027d571f | |||
| daaee43be3 | |||
| 0d71cb93af | |||
| e5e50e82d5 | |||
| 7e852124a5 | |||
| f66a49bc42 | |||
| baf78ccda2 | |||
| 31f0e66f45 | |||
| 28b78a563b | |||
| 2f380de73b | |||
| e3a9a39c9a | |||
| 1710bb78df | |||
| 3e13fc3e70 | |||
| befc399506 | |||
| 88116b9fb1 | |||
| 53e1c58cc5 | |||
| 4b9ecdd11d | |||
| e31e409ee8 | |||
| 5488aaf69f | |||
| 96e493d8b1 | |||
| e409453fbd | |||
| 309bf1348c | |||
| 76a5635298 | |||
| f4f2a1f6de | |||
| a440805ea1 | |||
| c359672bd2 | |||
| 38350935e6 | |||
| 421cd89a0f | |||
| 5ce3369aa6 | |||
| f38acfe988 | |||
| 965619d096 | |||
| 9f017e834c | |||
| 3c67b08488 | |||
| 4add755a4d | |||
| 56e249aee6 | |||
| 6a7c8fcfd5 | |||
| 14b1003c62 | |||
| 43a4bae010 | |||
| 9c205f77a2 | |||
| c2e4cfd832 | |||
| c008e1c5bc | |||
| 1aa60f0da3 | |||
| bd1fd8383c | |||
| aac54d0ea1 | |||
| 4fe718581b | |||
| 71842f07bd | |||
| f2bec1f82f | |||
| 10460191b9 | |||
| c5fffd6e2c | |||
| 951f63b6fd | |||
| e6d8932b3b | |||
| 70f96cca0a | |||
| 4e357e9659 | |||
| 1f8aed6732 | |||
| fa2bace3cd | |||
| 955039b5ea | |||
| 771ea1e815 | |||
| d38bfc4aff | |||
| fbb0054232 | |||
| 2d3c36edae | |||
| 8dcc41a54d | |||
| ba3d2e36c8 | |||
| b51047ffcc | |||
| b1c40a9079 | |||
| b014c267ae | |||
| 6b16cc52db | |||
| d35ad73e35 | |||
| 2a1af3d9ae | |||
| 82e30246c1 | |||
| bb3a05bb3f | |||
| 40fa82275c | |||
| 9824321fc9 | |||
| 27e607ab82 | |||
| a2b27b8790 | |||
| 396089ef0e | |||
| df98b5021d | |||
| 34ce6d0b02 | |||
| 7af937b08e | |||
| 8665003269 | |||
| 1e76716819 | |||
| 91a42fdf58 | |||
| 5ed5243be6 | |||
| 4560251e64 | |||
| 2020dca3e0 | |||
| 7fc2121454 | |||
| 8b84afbd38 | |||
| 305fc3b557 | |||
| 61f2ac01d7 | |||
| 39a9f55205 | |||
| 11f351dbeb | |||
| 815fa379ea | |||
| 4c480a1ea3 | |||
| fa4aa0e06d | |||
| e2a6374bf5 | |||
| dc14554053 | |||
| 985ca7b643 | |||
| 60624d64fa | |||
| 2935dae89e | |||
| 4c22c3285d | |||
| 93cee2994a | |||
| 9c7e8d04d2 | |||
| 1e6b8906e0 | |||
| 6c5b92e5c0 | |||
| 38c515e12e | |||
| c239937fac | |||
| bafa574784 | |||
| 199a5854a8 | |||
| a74a578198 | |||
| 7de752ec56 | |||
| 0a833171ac | |||
| 1a0612cbfd | |||
| fbbd3ba349 | |||
| 1028639186 | |||
| 0e5e764c78 | |||
| db1faecc95 | |||
| c2c415d2e8 | |||
| d193928f31 | |||
| 17861e0003 | |||
| 97fe964e00 | |||
| 9debb5db23 | |||
| 494b438151 | |||
| 010a236882 | |||
| 1951d2a9f2 | |||
| 9d8f640503 | |||
| b18cfbae23 | |||
| f64e7e14c3 | |||
| e8c9bfc06a | |||
| 07452f50a8 | |||
| 642c5acebb | |||
| 0886dedff1 | |||
| cc88a7d42e | |||
| c0829087da | |||
| b6f6d6a7c2 | |||
| 5ff8b89aaf | |||
| 927abad4b4 | |||
| 3d31f9860a | |||
| 8867a4f84c | |||
| 88f4c1d610 | |||
| ddcb5c5e10 | |||
| cd90dfc7be | |||
| a778ab3897 | |||
| 4c2f49d566 | |||
| 49d7052bb3 | |||
| 07be7e7eae | |||
| 97c8717d1e | |||
| 3ac0a751fe | |||
| 8b39f986d9 | |||
| 354c365a03 | |||
| e0ebf1bdff | |||
| 11633aef98 | |||
| 9193245871 | |||
| 7baf10b751 | |||
| f5d91c5ecc | |||
| 69e3edb5a3 | |||
| d58bb4eaa3 | |||
| c5fe25f422 | |||
| 600cffb009 | |||
| b9d14a9eda | |||
| 0e7e398df3 | |||
| 86bdc6898b | |||
| e5ca335115 | |||
| fce5d66878 | |||
| 05d218113c | |||
| ef6af6adc1 | |||
| 6632699e00 | |||
| d3e72245b0 | |||
| 13fe9c8ac3 | |||
| 6ecbf2db8a | |||
| c9be9056ef | |||
| 0866990b7d | |||
| f04befb567 | |||
| da3e5c4424 | |||
| 26ab4dfb87 | |||
| e887ee93a3 | |||
| d640e85158 | |||
| c8044a9b5d | |||
| 289ae3604d | |||
| 55fb885256 | |||
| 73a531f8bc | |||
| 10f04fd19d | |||
| 79fd309d6c | |||
| dd8b2be044 | |||
| 8d08782eba | |||
| 8555f37dbf | |||
| 4b837f429c | |||
| a480087618 | |||
| 84655d3b26 | |||
| 40843cbda1 | |||
| a13b9298c6 | |||
| 0c5e046820 | |||
| 907ebc4977 | |||
| e4161be1bf | |||
| be7fbd418f | |||
| 06ec9eecdb | |||
| 79eef5ee90 | |||
| 29602ca995 | |||
| d7156df842 | |||
| 33b39913c7 | |||
| d5cbc35811 | |||
| a038c5aaab | |||
| c9c985c927 | |||
| 859c0be0e5 | |||
| 810ea245f9 | |||
| 58fc5f3b06 | |||
| 7d4e99b760 | |||
| ab7d81aae0 | |||
| e24723125f | |||
| 03c603918d | |||
| 6fb60dacd2 | |||
| 42a9daec9d | |||
| 1ba2be3928 | |||
| 66be000410 | |||
| 5fc669c282 | |||
| 9b78b15ba5 | |||
| b9fd0a405e | |||
| 1b44e0cd20 | |||
| b3d4d4eacc | |||
| a835bdc940 | |||
| b258fd69d2 | |||
| 3ab3e778ab | |||
| e6203313ce | |||
| 938061dd5e | |||
| 0cca7a2116 | |||
| 39b46b3326 | |||
| 2aebd6bdbb | |||
| b501a9b303 | |||
| 94e5408f46 | |||
| eb190e3f94 | |||
| 80bb0d5876 | |||
| c04ccafd0a | |||
| 6ee5b5afa7 | |||
| 6a48728ffb | |||
| 9cb89ff26c | |||
| 4e5f392c50 | |||
| e35d9e760b | |||
| 22fee7b003 | |||
| e95d28e148 | |||
| 7a65a0b79f | |||
| ca30315deb | |||
| 9538e8f916 | |||
| 8b3715eabf | |||
| d0f2b9abd0 | |||
| 43578e21b1 | |||
| 55a798bd8b | |||
| cdcd5a2835 | |||
| 737e99ec69 | |||
| c3cb42f04d | |||
| d0e624e615 | |||
| 087a50a19c | |||
| 0bed253835 | |||
| 6b6a84e55b | |||
| 7d5785e96f | |||
| 70fa38fadf | |||
| 3514cd2e36 | |||
| 96083847fb | |||
| d25d6ce337 | |||
| bb044075fa | |||
| 370fd4e172 | |||
| 7dea3822a3 | |||
| 7d11ef0abb | |||
| dcb29efce5 | |||
| cb5d97f600 | |||
| 608ab7d8b1 | |||
| fd8ebb9d06 | |||
| 952916fd1c | |||
| a0592e8f53 | |||
| 5460c792bd | |||
| e5ecd27bbe | |||
| 4543873dae | |||
| a2c855315c | |||
| 6c4e4b374a | |||
| 9ab887bec8 | |||
| 268591f343 | |||
| a42717bcac | |||
| 6b013a08fc | |||
| b65a243fc9 | |||
| f0157e03e7 | |||
| 4b7c16b04a | |||
| aafd5ab70f | |||
| d8d6b5a5e0 | |||
| a1fd4b396f | |||
| 5521cdda63 | |||
| 12b16a9d7e | |||
| f7181fb066 | |||
| 17ac52e1d4 | |||
| 64a9351921 | |||
| 332af8b062 | |||
| b7901579d5 | |||
| 138c2a3bfd | |||
| 446a9f1e06 | |||
| 52265e2e19 | |||
| 0f522f209b | |||
| 30b213601a | |||
| 8eb34b2e18 | |||
| 74d1b1f406 | |||
| 2b3d196876 | |||
| 397b7e4bb9 | |||
| 598b27f83c | |||
| da53e79d07 | |||
| 2907d5af3e | |||
| dd919fe01b | |||
| f86a9bed1a | |||
| cfa87d508e | |||
| f19e1711a7 | |||
| 20cd4f5d04 | |||
| b2c7d3ad40 | |||
| 4832924483 | |||
| 28a8a9ace2 | |||
| a4f1caab1d | |||
| c8839f7658 | |||
| dfe3580607 | |||
| 1c02552e92 | |||
| ff7cbb97df | |||
| 09f3d3fb12 | |||
| 63defc25d2 | |||
| db39fc95f4 | |||
| 471dc714aa | |||
| fef665df73 | |||
| 7bfdf87bf0 | |||
| cf357d7058 | |||
| 618fa08aa5 | |||
| a40e7b4470 | |||
| f1894f6f9a | |||
| dfc2d452c5 | |||
| 66f23c3980 | |||
| 7a6ab31ad7 | |||
| 2f73dd5b59 | |||
| c658424c9f | |||
| bb58f2d162 | |||
| f54297f242 | |||
| b72d946062 | |||
| 883763c172 | |||
| 9063a5dbdc | |||
| 892e848985 | |||
| 0edb90bab2 | |||
| 8f71f8958e | |||
| fcb97cfd5e | |||
| 2983eb3113 | |||
| a968b1abc0 | |||
| 47c964d6fb | |||
| 22cb657ef1 | |||
| bb15d1e850 | |||
| 47680e43c5 | |||
| 0f1e44aac6 | |||
| 66aae91bca | |||
| 07bd76e219 | |||
| b6a7b3e9e4 | |||
| 1cf5cfce06 | |||
| 8ff90c4fc2 | |||
| 908c8eb42a | |||
| 0078293d4c | |||
| 9728dbeeac | |||
| 324029ca3b | |||
| 73be5b2ba1 | |||
| af904d23ac | |||
| ad84fc1479 | |||
| d5a8074b53 | |||
| c506fecc87 | |||
| d777810911 | |||
| bbdc07ee6c | |||
| 689338f059 | |||
| eee770514f | |||
| 5a0bda7ec4 | |||
| b454fd5d9e | |||
| 2a830ed498 | |||
| e98d1ec5a7 | |||
| 3ace97660f | |||
| 0824737757 | |||
| 8fdea033bc | |||
| 2e906fc5fa | |||
| a5a34934df | |||
| 08a8a0f29a | |||
| 519611c6c8 | |||
| a283c34dbb | |||
| f9fe86ee3e | |||
| 2e67152941 | |||
| 22bfec7259 | |||
| 1af9f9bd95 | |||
| 926451c8be | |||
| 7b3bef124d | |||
| 3be6e38af3 | |||
| f2290a43d9 | |||
| 4513663084 | |||
| 092cf6cfaf | |||
| 236f96e676 | |||
| 887ca971ab | |||
| 4cc195b681 | |||
| fc2be2b8d0 | |||
| 570bf1cb3c | |||
| 6ec9c72539 | |||
| 1a1086206c | |||
| f2766b103d | |||
| 62c9d44b04 | |||
| e394a7ff20 | |||
| 921ed63204 | |||
| 77dafb819f | |||
| 1da99f4003 | |||
| 120eaa6c56 | |||
| fb636ef98d | |||
| 6147a31b48 | |||
| 3f8707496f | |||
| de6caec685 | |||
| c8411e55d9 | |||
| d3aebdbec4 | |||
| a56e4ff436 | |||
| 9dcc9160b3 | |||
| 43df7fad46 | |||
| d2087a2cd9 | |||
| c681198179 | |||
| 105938df0b | |||
| 7b6fa12854 | |||
| e7c201abba | |||
| 4fd04951e6 | |||
| 747c186293 | |||
| bdf9894020 | |||
| d180e60e05 | |||
| 65addefd09 | |||
| 697fcbac12 | |||
| a8e281e95f | |||
| 4d60eae82e | |||
| 2b5215c244 | |||
| a43f30b7f5 | |||
| 88f7b08e56 | |||
| dc92d80b9f | |||
| 0757ad08e7 | |||
| 5577021475 | |||
| 40aff3a094 | |||
| 6c5f10035a | |||
| 96d2baa2b5 | |||
| 5d2754f831 | |||
| ebaf1b0620 | |||
| 589e5a600c | |||
| 198b5a502d | |||
| cb0ebd35ce | |||
| 29cf80a3dd | |||
| db89d4d3dd | |||
| 226273f660 | |||
| c0ded35783 | |||
| 39632e9c1e | |||
| 66202992c9 | |||
| eb59b10050 | |||
| 986f2c14ab | |||
| 793e1bdbc5 | |||
| d62721d5f8 | |||
| d54619e1d1 | |||
| 8425493ef5 | |||
| 6121e64338 | |||
| 33b5beaeee | |||
| 1dae45c58d | |||
| 997119c443 | |||
| 032589446a | |||
| 9ae98e09cb | |||
| 2ffa1ae705 | |||
| fee72b87cf | |||
| 6c47bd6e80 | |||
| 02c2972e74 | |||
| 4b830ee7ff | |||
| 8e41568ffd | |||
| dbe810d3d8 | |||
| a1563b9132 | |||
| 98aea9579f | |||
| 7019172b67 | |||
| be62bd123a | |||
| 3c63be6261 | |||
| e3406ac255 | |||
| 22a948cc75 | |||
| bc3d6cac80 | |||
| a55e385b12 | |||
| af6d84a7f8 | |||
| f203c8729a | |||
| dbf0dddfcc | |||
| c6c17cccac | |||
| b5ad0e12fd | |||
| c8e46b9d17 | |||
| f2ce84b243 | |||
| ae7fb4c4f4 | |||
| 4746a0da7d | |||
| 2ac8d84034 | |||
| eb0f7aa429 | |||
| bcca03cce7 | |||
| efb39e466b | |||
| 14d637f4ef | |||
| c9d90afe59 | |||
| d088ce248f | |||
| f4cdde1f4f | |||
| 56e02a398d | |||
| 2552b129c4 |
@@ -1 +1,12 @@
|
||||
# LFS configuration for images from the wiki
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Exclude LFS-tracked files from the tarball
|
||||
/wiki/img/ export-ignore
|
||||
|
||||
# exclude .gitattributes itself from the tarball
|
||||
.gitattributes export-ignore
|
||||
|
||||
# tip: can be tested using
|
||||
# git archive --format=tar.gz --output=source.tar.gz HEAD && \
|
||||
# tar tfvz source.tar.gz | grep -e '.png' -e '.gitattributes'
|
||||
|
||||
@@ -9,9 +9,23 @@ assignees: ''
|
||||
|
||||
<!-- Please describe the issue here at the top, then fill in the system information below. -->
|
||||
|
||||
<!-- Attaching your full niri config can help diagnose the problem. -->
|
||||
|
||||
<!--
|
||||
If you have a problem with a specific app, please verify that it is running on Wayland, rather than X11. An easy way is to run xeyes and mouse over the app: xeyes will be able to "see" only X11 windows.
|
||||
|
||||
You can also check what process the window PID belongs to:
|
||||
|
||||
$ readlink /proc/$(niri msg --json pick-window | jq .pid)/exe
|
||||
|
||||
If this points to xwayland-satellite, then it's an X11 window.
|
||||
|
||||
Please report issues with X11 apps to xwayland-satellite instead of niri: https://github.com/Supreeeme/xwayland-satellite/issues
|
||||
-->
|
||||
|
||||
### System Information
|
||||
|
||||
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
|
||||
<!-- Paste the output of `niri -V`, e.g. niri 25.02 (b94a5db) -->
|
||||
* niri version:
|
||||
|
||||
<!-- Write your distribution, e.g. Fedora 40 Silverblue -->
|
||||
|
||||
@@ -2,3 +2,9 @@ contact_links:
|
||||
- name: Feature request
|
||||
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
|
||||
about: Ideas for new features and functionality (start a Discussion)
|
||||
- name: Ask a question
|
||||
url: https://github.com/YaLTeR/niri/discussions/new?category=q-a
|
||||
about: Question about niri (start a Discussion)
|
||||
- name: Matrix room
|
||||
url: https://matrix.to/#/#niri:matrix.org
|
||||
about: Chat about niri with other users
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
smithay:
|
||||
patterns:
|
||||
- "smithay"
|
||||
- "smithay-drm-extras"
|
||||
rust-dependencies:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
ignore:
|
||||
- dependency-name: "Andrew-Chen-Wang/github-wiki-action"
|
||||
+103
-35
@@ -9,6 +9,8 @@ on:
|
||||
|
||||
env:
|
||||
RUN_SLOW_TESTS: 1
|
||||
DEPS_APT: curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
|
||||
DEPS_DNF: cargo gcc clang libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel libdisplay-info-devel
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -23,8 +25,7 @@ jobs:
|
||||
release-flag: '--release'
|
||||
|
||||
name: test - ${{ matrix.configuration }}
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -33,8 +34,8 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update -y
|
||||
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }}
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -63,19 +64,26 @@ jobs:
|
||||
- name: Build (with profiling)
|
||||
run: cargo build ${{ matrix.release-flag }} --features profile-with-tracy
|
||||
|
||||
- name: Build Tests
|
||||
- name: Build tests
|
||||
run: cargo test --no-run --all --exclude niri-visual-tests ${{ matrix.release-flag }}
|
||||
|
||||
- name: Test
|
||||
run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
|
||||
|
||||
visual-tests:
|
||||
# Job that runs randomized tests for a longer period of time.
|
||||
randomized-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: visual tests
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
name: randomized tests
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
PROPTEST_CASES: 200000
|
||||
PROPTEST_MAX_LOCAL_REJECTS: 200000
|
||||
PROPTEST_MAX_GLOBAL_REJECTS: 200000
|
||||
PROPTEST_MAX_SHRINK_ITERS: 200000
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -84,8 +92,35 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update -y
|
||||
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }}
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build tests
|
||||
run: cargo test --no-run --all --exclude niri-visual-tests --release
|
||||
|
||||
- name: Test
|
||||
run: cargo test --all --exclude niri-visual-tests --release
|
||||
|
||||
visual-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: visual tests
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -98,9 +133,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: 'msrv - 1.72.0'
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
name: msrv
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -109,10 +143,10 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update -y
|
||||
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@1.72.0
|
||||
- uses: dtolnay/rust-toolchain@1.80.1
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
@@ -123,8 +157,7 @@ jobs:
|
||||
fail-fast: false
|
||||
|
||||
name: clippy
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -133,8 +166,8 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update -y
|
||||
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -146,25 +179,23 @@ jobs:
|
||||
run: cargo clippy --all --all-targets
|
||||
|
||||
rustfmt:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set auto-self-update check-only
|
||||
rustup toolchain install nightly --profile minimal --component rustfmt
|
||||
rustup override set nightly
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt
|
||||
|
||||
- name: Run rustfmt
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
fedora:
|
||||
runs-on: ubuntu-22.04
|
||||
container: fedora:39
|
||||
runs-on: ubuntu-24.04
|
||||
container: fedora:41
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -174,13 +205,13 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo dnf update -y
|
||||
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libadwaita-devel
|
||||
sudo dnf install -y ${{ env.DEPS_DNF }} libadwaita-devel
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo build --all
|
||||
|
||||
nix:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -194,18 +225,55 @@ jobs:
|
||||
uses: DeterminateSystems/nix-installer-action@v3
|
||||
continue-on-error: true
|
||||
|
||||
- run: nix build
|
||||
- run: nix flake check
|
||||
continue-on-error: true
|
||||
|
||||
check-links:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: lycheeverse/lychee-action@v2.0.2 # later versions break fragment checks. don't bump until this is fixed: https://github.com/lycheeverse/lychee/issues/1574
|
||||
with:
|
||||
args: --offline --include-fragments 'wiki/*.md'
|
||||
|
||||
publish-wiki:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs: build
|
||||
needs:
|
||||
- build
|
||||
- check-links
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
show-progress: false
|
||||
- uses: Andrew-Chen-Wang/github-wiki-action@86138cbd6328b21d759e89ab6e6dd6a139b22270
|
||||
- uses: Andrew-Chen-Wang/github-wiki-action@b7e552d7cb0fa7f83e459012ffc6840fd87bcb83
|
||||
|
||||
rustdoc:
|
||||
needs: build
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Generate documentation
|
||||
run: cargo doc --no-deps -p niri-ipc
|
||||
|
||||
- run: cp ./resources/rustdoc-index.html ./target/doc/index.html
|
||||
|
||||
- name: Deploy documentation
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./target/doc
|
||||
force_orphan: true
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Prepare release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Public version'
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
RUN_SLOW_TESTS: 1
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Check for unreplaced "Since:" in the wiki
|
||||
run: |
|
||||
if grep --recursive 'Since: next release' wiki; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Create vendored dependencies archive
|
||||
run: |
|
||||
mkdir .cargo
|
||||
cargo vendor --locked > .cargo/config.toml
|
||||
tar cJf niri-${{ github.event.inputs.version }}-vendored-dependencies.tar.xz vendor/
|
||||
|
||||
- name: Build
|
||||
run: cargo build --all --frozen --release
|
||||
|
||||
- name: Build tests
|
||||
run: cargo test --no-run --all --frozen --release
|
||||
|
||||
- name: Test
|
||||
run: cargo test --all --frozen --release -- --nocapture
|
||||
|
||||
- name: Draft release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
tag_name: v${{ github.event.inputs.version }}
|
||||
files: niri-${{ github.event.inputs.version }}-vendored-dependencies.tar.xz
|
||||
fail_on_unmatched_files: true
|
||||
Generated
+1767
-1260
File diff suppressed because it is too large
Load Diff
+78
-40
@@ -1,30 +1,38 @@
|
||||
[workspace]
|
||||
members = ["niri-visual-tests"]
|
||||
members = [
|
||||
"niri-config",
|
||||
"niri-ipc",
|
||||
"niri-visual-tests",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.6"
|
||||
version = "25.5.1"
|
||||
description = "A scrollable-tiling Wayland compositor"
|
||||
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/YaLTeR/niri"
|
||||
rust-version = "1.80.1"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.83"
|
||||
bitflags = "2.5.0"
|
||||
clap = { version = "~4.4.18", features = ["derive"] }
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
serde_json = "1.0.117"
|
||||
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
tracy-client = { version = "0.17.0", default-features = false }
|
||||
anyhow = "1.0.98"
|
||||
bitflags = "2.9.1"
|
||||
clap = { version = "4.5.38", features = ["derive"] }
|
||||
insta = "1.43.1"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
tracy-client = { version = "0.18.0", default-features = false }
|
||||
|
||||
[workspace.dependencies.smithay]
|
||||
# version = "0.4.1"
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
# path = "../smithay"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.smithay-drm-extras]
|
||||
# version = "0.1.0"
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
# path = "../smithay/smithay-drm-extras"
|
||||
|
||||
@@ -36,47 +44,54 @@ authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
readme = "README.md"
|
||||
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
arrayvec = "0.7.4"
|
||||
arrayvec = "0.7.6"
|
||||
async-channel = "2.3.1"
|
||||
async-io = { version = "1.13.0", optional = true }
|
||||
async-io = { version = "2.4.0", optional = true }
|
||||
atomic = "0.6.0"
|
||||
bitflags.workspace = true
|
||||
bytemuck = { version = "1.16.0", features = ["derive"] }
|
||||
calloop = { version = "0.13.0", features = ["executor", "futures-io"] }
|
||||
bytemuck = { version = "1.23.0", features = ["derive"] }
|
||||
calloop = { version = "0.14.2", features = ["executor", "futures-io"] }
|
||||
clap = { workspace = true, features = ["string"] }
|
||||
directories = "5.0.1"
|
||||
drm-ffi = "0.8.0"
|
||||
fastrand = "2.1.0"
|
||||
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
|
||||
clap_complete = "4.5.50"
|
||||
directories = "6.0.0"
|
||||
drm-ffi = "0.9.0"
|
||||
fastrand = "2.3.0"
|
||||
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
|
||||
git-version = "0.3.9"
|
||||
glam = "0.27.0"
|
||||
input = { version = "0.9.0", features = ["libinput_1_21"] }
|
||||
glam = "0.30.3"
|
||||
input = { version = "0.9.1", features = ["libinput_1_21"] }
|
||||
keyframe = { version = "1.1.1", default-features = false }
|
||||
libc = "0.2.154"
|
||||
log = { version = "0.4.21", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
niri-config = { version = "0.1.6", path = "niri-config" }
|
||||
niri-ipc = { version = "0.1.6", path = "niri-ipc", features = ["clap"] }
|
||||
notify-rust = { version = "~4.10.0", optional = true }
|
||||
pangocairo = "0.19.2"
|
||||
pipewire = { version = "0.8.0", optional = true }
|
||||
png = "0.17.13"
|
||||
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
|
||||
profiling = "1.0.15"
|
||||
sd-notify = "0.4.1"
|
||||
libc = "0.2.172"
|
||||
libdisplay-info = "0.2.2"
|
||||
log = { version = "0.4.27", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
niri-config = { version = "25.5.1", path = "niri-config" }
|
||||
niri-ipc = { version = "25.5.1", path = "niri-ipc", features = ["clap"] }
|
||||
ordered-float = "5.0.0"
|
||||
pango = { version = "0.20.10", features = ["v1_44"] }
|
||||
pangocairo = "0.20.10"
|
||||
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
|
||||
png = "0.17.16"
|
||||
portable-atomic = { version = "1.11.0", default-features = false, features = ["float"] }
|
||||
profiling = "1.0.16"
|
||||
sd-notify = "0.4.5"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smithay-drm-extras.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
url = { version = "2.5.0", optional = true }
|
||||
xcursor = "0.3.5"
|
||||
zbus = { version = "~3.15.2", optional = true }
|
||||
url = { version = "2.5.4", optional = true }
|
||||
wayland-backend = "0.3.10"
|
||||
wayland-scanner = "0.31.6"
|
||||
xcursor = "0.3.8"
|
||||
zbus = { version = "5.7.0", optional = true }
|
||||
|
||||
[dependencies.smithay]
|
||||
workspace = true
|
||||
@@ -97,21 +112,29 @@ features = [
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
k9 = "0.12.0"
|
||||
proptest = "1.4.0"
|
||||
proptest-derive = "0.4.0"
|
||||
xshell = "0.2.6"
|
||||
approx = "0.5.1"
|
||||
calloop-wayland-source = "0.4.0"
|
||||
insta.workspace = true
|
||||
proptest = "1.6.0"
|
||||
proptest-derive = { version = "0.5.1", features = ["boxed_union"] }
|
||||
rayon = "1.10.0"
|
||||
wayland-client = "0.31.10"
|
||||
xshell = "0.2.7"
|
||||
|
||||
[features]
|
||||
default = ["dbus", "systemd", "xdp-gnome-screencast"]
|
||||
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
|
||||
dbus = ["zbus", "async-io", "notify-rust", "url"]
|
||||
dbus = ["dep:zbus", "dep:async-io", "dep:url"]
|
||||
# Enables systemd integration (global environment, apps in transient scopes).
|
||||
systemd = ["dbus"]
|
||||
# Enables screencasting support through xdg-desktop-portal-gnome.
|
||||
xdp-gnome-screencast = ["dbus", "pipewire"]
|
||||
# Enables the Tracy profiler instrumentation.
|
||||
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
|
||||
# Enables the on-demand Tracy profiler instrumentation.
|
||||
profile-with-tracy-ondemand = ["profile-with-tracy", "tracy-client/ondemand", "tracy-client/manual-lifetime"]
|
||||
# Enables Tracy allocation profiling.
|
||||
profile-with-tracy-allocations = ["profile-with-tracy"]
|
||||
# Enables dinit integration (global environment).
|
||||
dinit = []
|
||||
|
||||
@@ -124,8 +147,12 @@ lto = "thin"
|
||||
# knuffel with chomsky generates a metric ton of debuginfo.
|
||||
debug = false
|
||||
|
||||
[profile.dev.package]
|
||||
insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
version = "0.1.6"
|
||||
version = "25.02"
|
||||
assets = [
|
||||
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
||||
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
||||
@@ -137,3 +164,14 @@ assets = [
|
||||
[package.metadata.generate-rpm.requires]
|
||||
alacritty = "*"
|
||||
fuzzel = "*"
|
||||
|
||||
[package.metadata.deb]
|
||||
depends = "alacritty, fuzzel"
|
||||
assets = [
|
||||
["target/release/niri", "usr/bin/", "755"],
|
||||
["resources/niri-session", "usr/bin/", "755"],
|
||||
["resources/niri.desktop", "/usr/share/wayland-sessions/", "644"],
|
||||
["resources/niri-portals.conf", "/usr/share/xdg-desktop-portal/", "644"],
|
||||
["resources/niri.service", "/usr/lib/systemd/user/", "644"],
|
||||
["resources/niri-shutdown.target", "/usr/lib/systemd/user/", "644"],
|
||||
]
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<h1 align="center">niri</h1>
|
||||
<p align="center">A scrollable-tiling Wayland compositor.</p>
|
||||
<p align="center">
|
||||
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/matrix/niri%3Amatrix.org?logo=matrix&label=matrix"></a>
|
||||
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/badge/matrix-%23niri-blue?logo=matrix"></a>
|
||||
<a href="https://github.com/YaLTeR/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/YaLTeR/niri"></a>
|
||||
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/YaLTeR/niri/wiki/Getting-Started">Getting Started</a> | <a href="https://github.com/YaLTeR/niri/wiki/Configuration:-Overview">Configuration</a>
|
||||
<a href="https://github.com/YaLTeR/niri/wiki/Getting-Started">Getting Started</a> | <a href="https://github.com/YaLTeR/niri/wiki/Configuration:-Overview">Configuration</a> | <a href="https://github.com/YaLTeR/niri/discussions/325">Setup Showcase</a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||
|
||||
## About
|
||||
|
||||
@@ -28,13 +28,17 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
|
||||
|
||||
## Features
|
||||
|
||||
- Scrollable tiling
|
||||
- Dynamic workspaces like in GNOME
|
||||
- Built from the ground up for scrollable tiling
|
||||
- [Dynamic workspaces](https://github.com/YaLTeR/niri/wiki/Workspaces) like in GNOME
|
||||
- An [Overview](https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995) that zooms out workspaces and windows
|
||||
- Built-in screenshot UI
|
||||
- Monitor screencasting through xdg-desktop-portal-gnome
|
||||
- Monitor and window screencasting through xdg-desktop-portal-gnome
|
||||
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
|
||||
- [Dynamic cast target](https://github.com/YaLTeR/niri/wiki/Screencasting#dynamic-screencast-target) that can change what it shows on the go
|
||||
- [Touchpad](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/YaLTeR/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
|
||||
- Group windows into [tabs](https://github.com/YaLTeR/niri/wiki/Tabs)
|
||||
- Configurable layout: gaps, borders, struts, window sizes
|
||||
- [Gradient borders](https://github.com/YaLTeR/niri/wiki/Configuration:-Layout#gradients) with Oklab and Oklch support
|
||||
- [Animations](https://github.com/YaLTeR/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/YaLTeR/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
|
||||
- Live-reloading config
|
||||
|
||||
@@ -44,11 +48,34 @@ https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f97
|
||||
|
||||
## Status
|
||||
|
||||
A lot of the essential functionality is implemented, plus some goodies on top.
|
||||
Feel free to give niri a try: follow the instructions on the [Getting Started](https://github.com/YaLTeR/niri/wiki/Getting-Started) wiki page.
|
||||
Niri is stable for day-to-day use and does most things expected of a Wayland compositor.
|
||||
Many people are daily-driving niri, and are happy to help in our [Matrix channel].
|
||||
|
||||
Give it a try!
|
||||
Follow the instructions on the [Getting Started](https://github.com/YaLTeR/niri/wiki/Getting-Started) wiki page.
|
||||
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
|
||||
|
||||
Note that NVIDIA GPUs may have issues.
|
||||
Here are some points you may have questions about:
|
||||
|
||||
- **Multi-monitor**: yes, a core part of the design from the very start. Mixed DPI works.
|
||||
- **Fractional scaling**: yes, plus all niri UI stays pixel-perfect.
|
||||
- **NVIDIA**: seems to work fine.
|
||||
- **Floating windows**: yes, starting from niri 25.01.
|
||||
- **Input devices**: niri supports tablets, touchpads, and touchscreens.
|
||||
You can map the tablet to a specific monitor, or use [OpenTabletDriver].
|
||||
We have touchpad gestures, but no touchscreen gestures yet.
|
||||
- **Wlr protocols**: yes, we have most of the important ones like layer-shell, gamma-control, screencopy.
|
||||
You can check on [wayland.app](https://wayland.app) at the bottom of each protocol's page.
|
||||
- **Performance**: while I run niri on beefy machines, I try to stay conscious of performance.
|
||||
I've seen someone use it fine on an Eee PC 900 from 2008, of all things.
|
||||
- **Xwayland**: no built-in support, but xwayland-satellite is [easy to set up](https://github.com/YaLTeR/niri/wiki/Xwayland#using-xwayland-satellite) and works very well.
|
||||
- Steam and games, including Proton: work perfectly through xwayland-satellite.
|
||||
- JetBrains IDEs, Ghidra: work well through xwayland-satellite.
|
||||
- Discord and other Electron apps: work well through xwayland-satellite.
|
||||
- Chromium and VSCode: work perfectly natively on Wayland with the right flags.
|
||||
- X11 apps that want to position windows or bars at specific screen coordinates: won't work well; you can run them in a nested compositor like [labwc](https://github.com/YaLTeR/niri/wiki/Xwayland#using-the-labwc-wayland-compositor) or [rootful Xwayland](https://github.com/YaLTeR/niri/wiki/Xwayland#directly-running-xwayland-in-rootful-mode).
|
||||
- Display scaling (integer or fractional) keeps X11 apps crisp, but you need the latest xwayland-satellite.
|
||||
For games, you can run them in [gamescope] at native resolution, even with display scaling.
|
||||
|
||||
## Inspiration
|
||||
|
||||
@@ -57,6 +84,23 @@ Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top
|
||||
One of the reasons that prompted me to try writing my own compositor is being able to properly separate the monitors.
|
||||
Being a GNOME Shell extension, PaperWM has to work against Shell's global window coordinate space to prevent windows from overflowing.
|
||||
|
||||
## Tile Scrollably Elsewhere
|
||||
|
||||
Here are some other projects which implement a similar workflow:
|
||||
|
||||
- [PaperWM]: scrollable tiling on top of GNOME Shell.
|
||||
- [karousel]: scrollable tiling on top of KDE.
|
||||
- [scroll](https://github.com/dawsers/scroll) and [papersway]: scrollable tiling on top of sway/i3.
|
||||
- [hyprscrolling] and [hyprslidr]: scrollable tiling on top of Hyprland.
|
||||
- [PaperWM.spoon]: scrollable tiling on top of macOS.
|
||||
|
||||
## Media
|
||||
|
||||
[niri: Making a Wayland compositor in Rust](https://youtu.be/Kmz8ODolnDg?list=PLRdS-n5seLRqrmWDQY4KDqtRMfIwU0U3T)
|
||||
|
||||
My talk from the 2024 Moscow RustCon about niri, and how I do randomized property testing and profiling, and measure input latency.
|
||||
The talk is in Russian, but I prepared full English subtitles that you can find in YouTube's subtitle language selector.
|
||||
|
||||
## Contact
|
||||
|
||||
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
|
||||
@@ -64,4 +108,11 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
|
||||
[PaperWM]: https://github.com/paperwm/PaperWM
|
||||
[waybar]: https://github.com/Alexays/Waybar
|
||||
[fuzzel]: https://codeberg.org/dnkl/fuzzel
|
||||
|
||||
[karousel]: https://github.com/peterfajdiga/karousel
|
||||
[papersway]: https://spwhitton.name/tech/code/papersway/
|
||||
[hyprscrolling]: https://github.com/hyprwm/hyprland-plugins/tree/main/hyprscrolling
|
||||
[hyprslidr]: https://gitlab.com/magus/hyprslidr
|
||||
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
|
||||
[Matrix channel]: https://matrix.to/#/#niri:matrix.org
|
||||
[OpenTabletDriver]: https://opentabletdriver.net/
|
||||
[gamescope]: https://github.com/ValveSoftware/gamescope
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ignore-interior-mutability = [
|
||||
"smithay::desktop::Window",
|
||||
"smithay::output::Output",
|
||||
"wayland_server::backend::ClientId",
|
||||
]
|
||||
Generated
+21
-95
@@ -1,72 +1,12 @@
|
||||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709610799,
|
||||
"narHash": "sha256-5jfLQx0U9hXbi2skYMGodDJkIgffrjIOgMRjZqms2QE=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "81c393c776d5379c030607866afef6406ca1be57",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709274179,
|
||||
"narHash": "sha256-O6EC6QELBLHzhdzBOJj0chx8AOcd4nDRECIagfT5Nd0=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "4be608f4f81d351aacca01b21ffd91028c23cc22",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "monthly",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709126324,
|
||||
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-filter": {
|
||||
"locked": {
|
||||
"lastModified": 1705332318,
|
||||
"narHash": "sha256-kcw1yFeJe9N4PjQji9ZeX47jg0p9A0DuU4djKvg1a7I=",
|
||||
"lastModified": 1731533336,
|
||||
"narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"rev": "3449dc925982ad46246cfc36469baf66e1b64f17",
|
||||
"rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -77,11 +17,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1709386671,
|
||||
"narHash": "sha256-VPqfBnIJ+cfa78pd4Y5Cr6sOWVW8GYHRVucxJGmRf8Q=",
|
||||
"lastModified": 1742707865,
|
||||
"narHash": "sha256-RVQQZy38O3Zb8yoRJhuFgWo/iDIDj0hEdRTVfhOtzRk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "fa9a51752f1b5de583ad5213eb621be071806663",
|
||||
"rev": "dd613136ee91f67e5dba3f3f41ac99ae89c5406b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -93,42 +33,28 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils",
|
||||
"nix-filter": "nix-filter",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709219524,
|
||||
"narHash": "sha256-8HHRXm4kYQLdUohNDUuCC3Rge7fXrtkjBUf0GERxrkM=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "9efa23c4dacee88b93540632eb3d88c5dfebfe17",
|
||||
"lastModified": 1742697269,
|
||||
"narHash": "sha256-Lpp0XyAtIl1oGJzNmTiTGLhTkcUjwSkEb0gOiNzYFGM=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "01973c84732f9275c50c5f075dd1f54cc04b3316",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,108 +1,261 @@
|
||||
# This flake file is community maintained
|
||||
# Maintainers:
|
||||
# Bill Sun (github/billksun)
|
||||
{
|
||||
description = "Niri: A scrollable-tiling Wayland compositor.";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
crane = {
|
||||
url = "github:ipetkov/crane";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nix-filter.url = "github:numtide/nix-filter";
|
||||
fenix = {
|
||||
url = "github:nix-community/fenix/monthly";
|
||||
|
||||
# NOTE: This is not necessary for end users
|
||||
# You can omit it with `inputs.rust-overlay.follows = ""`
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
crane,
|
||||
nix-filter,
|
||||
flake-utils,
|
||||
fenix,
|
||||
...
|
||||
}: let
|
||||
systems = ["aarch64-linux" "x86_64-linux"];
|
||||
in
|
||||
flake-utils.lib.eachSystem systems (
|
||||
system: let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
toolchain = fenix.packages.${system}.complete.toolchain;
|
||||
craneLib = crane.lib.${system}.overrideToolchain toolchain;
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
nix-filter,
|
||||
rust-overlay,
|
||||
}:
|
||||
let
|
||||
niri-package =
|
||||
{
|
||||
lib,
|
||||
cairo,
|
||||
dbus,
|
||||
libGL,
|
||||
libdisplay-info,
|
||||
libinput,
|
||||
seatd,
|
||||
libxkbcommon,
|
||||
libgbm,
|
||||
pango,
|
||||
pipewire,
|
||||
pkg-config,
|
||||
rustPlatform,
|
||||
systemd,
|
||||
wayland,
|
||||
installShellFiles,
|
||||
withDbus ? true,
|
||||
withSystemd ? true,
|
||||
withScreencastSupport ? true,
|
||||
withDinit ? false,
|
||||
}:
|
||||
|
||||
craneArgs = {
|
||||
rustPlatform.buildRustPackage {
|
||||
pname = "niri";
|
||||
version = self.rev or "dirty";
|
||||
version = self.shortRev or self.dirtyShortRev or "unknown";
|
||||
|
||||
src = nixpkgs.lib.cleanSourceWith {
|
||||
src = craneLib.path ./.;
|
||||
filter = path: type:
|
||||
(builtins.match "resources" path == null) ||
|
||||
((craneLib.filterCargoSources path type) &&
|
||||
(builtins.match "niri-visual-tests" path == null));
|
||||
src = nix-filter.lib.filter {
|
||||
root = self;
|
||||
include = [
|
||||
"niri-config"
|
||||
"niri-ipc"
|
||||
"niri-visual-tests"
|
||||
"resources"
|
||||
"src"
|
||||
./Cargo.lock
|
||||
./Cargo.toml
|
||||
];
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
postPatch = ''
|
||||
patchShebangs resources/niri-session
|
||||
substituteInPlace resources/niri.service \
|
||||
--replace-fail '/usr/bin' "$out/bin"
|
||||
'';
|
||||
|
||||
cargoLock = {
|
||||
# NOTE: This is only used for Git dependencies
|
||||
allowBuiltinFetchGit = true;
|
||||
lockFile = ./Cargo.lock;
|
||||
};
|
||||
|
||||
strictDeps = true;
|
||||
|
||||
nativeBuildInputs = [
|
||||
rustPlatform.bindgenHook
|
||||
pkg-config
|
||||
autoPatchelfHook
|
||||
clang
|
||||
gdk-pixbuf
|
||||
graphene
|
||||
gtk4
|
||||
libadwaita
|
||||
installShellFiles
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
wayland
|
||||
systemd # For libudev
|
||||
seatd # For libseat
|
||||
libxkbcommon
|
||||
libinput
|
||||
mesa # For libgbm
|
||||
fontconfig
|
||||
stdenv.cc.cc.lib
|
||||
pipewire
|
||||
pango
|
||||
];
|
||||
buildInputs =
|
||||
[
|
||||
cairo
|
||||
dbus
|
||||
libGL
|
||||
libdisplay-info
|
||||
libinput
|
||||
seatd
|
||||
libxkbcommon
|
||||
libgbm
|
||||
pango
|
||||
wayland
|
||||
]
|
||||
++ lib.optional (withDbus || withScreencastSupport || withSystemd) dbus
|
||||
++ lib.optional withScreencastSupport pipewire
|
||||
# Also includes libudev
|
||||
++ lib.optional withSystemd systemd;
|
||||
|
||||
runtimeDependencies = with pkgs; [
|
||||
wayland
|
||||
mesa
|
||||
libglvnd # For libEGL
|
||||
xorg.libXcursor
|
||||
xorg.libXi
|
||||
];
|
||||
buildFeatures =
|
||||
lib.optional withDbus "dbus"
|
||||
++ lib.optional withDinit "dinit"
|
||||
++ lib.optional withScreencastSupport "xdp-gnome-screencast"
|
||||
++ lib.optional withSystemd "systemd";
|
||||
buildNoDefaultFeatures = true;
|
||||
|
||||
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
|
||||
# ever since this commit:
|
||||
# https://github.com/YaLTeR/niri/commit/771ea1e81557ffe7af9cbdbec161601575b64d81
|
||||
# niri now runs an actual instance of the real compositor (with a mock backend) during tests
|
||||
# and thus creates a real socket file in the runtime dir.
|
||||
# this is fine for our build, we just need to make sure it has a directory to write to.
|
||||
preCheck = ''
|
||||
export XDG_RUNTIME_DIR="$(mktemp -d)"
|
||||
'';
|
||||
|
||||
postInstall =
|
||||
''
|
||||
installShellCompletion --cmd niri \
|
||||
--bash <($out/bin/niri completions bash) \
|
||||
--fish <($out/bin/niri completions fish) \
|
||||
--zsh <($out/bin/niri completions zsh)
|
||||
|
||||
install -Dm644 resources/niri.desktop -t $out/share/wayland-sessions
|
||||
install -Dm644 resources/niri-portals.conf -t $out/share/xdg-desktop-portal
|
||||
''
|
||||
+ lib.optionalString withSystemd ''
|
||||
install -Dm755 resources/niri-session $out/bin/niri-session
|
||||
install -Dm644 resources/niri{.service,-shutdown.target} -t $out/share/systemd/user
|
||||
'';
|
||||
|
||||
env = {
|
||||
# Force linking with libEGL and libwayland-client
|
||||
# so they can be discovered by `dlopen()`
|
||||
RUSTFLAGS = toString (
|
||||
map (arg: "-C link-arg=" + arg) [
|
||||
"-Wl,--push-state,--no-as-needed"
|
||||
"-lEGL"
|
||||
"-lwayland-client"
|
||||
"-Wl,--pop-state"
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
passthru = {
|
||||
providedSessions = [ "niri" ];
|
||||
};
|
||||
|
||||
meta = {
|
||||
description = "Scrollable-tiling Wayland compositor";
|
||||
homepage = "https://github.com/YaLTeR/niri";
|
||||
license = lib.licenses.gpl3Only;
|
||||
mainProgram = "niri";
|
||||
platforms = lib.platforms.linux;
|
||||
};
|
||||
};
|
||||
|
||||
cargoArtifacts = craneLib.buildDepsOnly craneArgs;
|
||||
niri = craneLib.buildPackage (craneArgs // {inherit cargoArtifacts;});
|
||||
in {
|
||||
formatter = pkgs.alejandra;
|
||||
inherit (nixpkgs) lib;
|
||||
# Support all Linux systems that the nixpkgs flake exposes
|
||||
systems = lib.intersectLists lib.systems.flakeExposed lib.platforms.linux;
|
||||
|
||||
checks.niri = niri;
|
||||
packages.default = niri;
|
||||
forAllSystems = lib.genAttrs systems;
|
||||
nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system});
|
||||
in
|
||||
{
|
||||
checks = forAllSystems (system: {
|
||||
# We use the debug build here to save a bit of time
|
||||
inherit (self.packages.${system}) niri-debug;
|
||||
});
|
||||
|
||||
devShells.default = pkgs.mkShell.override {stdenv = pkgs.clangStdenv;} {
|
||||
inherit (niri) nativeBuildInputs buildInputs LIBCLANG_PATH;
|
||||
packages = niri.runtimeDependencies;
|
||||
devShells = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgsFor.${system};
|
||||
rust-bin = rust-overlay.lib.mkRustBin { } pkgs;
|
||||
inherit (self.packages.${system}) niri;
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = [
|
||||
# We don't use the toolchain from nixpkgs
|
||||
# because we prefer a nightly toolchain
|
||||
# and we *require* a nightly rustfmt
|
||||
(rust-bin.selectLatestNightlyWith (
|
||||
toolchain:
|
||||
toolchain.default.override {
|
||||
extensions = [
|
||||
# includes already:
|
||||
# rustc
|
||||
# cargo
|
||||
# rust-std
|
||||
# rust-docs
|
||||
# rustfmt-preview
|
||||
# clippy-preview
|
||||
"rust-analyzer"
|
||||
"rust-src"
|
||||
];
|
||||
}
|
||||
))
|
||||
];
|
||||
|
||||
# Force linking to libEGL, which is always dlopen()ed, and to
|
||||
# libwayland-client, which is always dlopen()ed except by the
|
||||
# obscure winit backend.
|
||||
RUSTFLAGS = map (a: "-C link-arg=${a}") [
|
||||
"-Wl,--push-state,--no-as-needed"
|
||||
"-lEGL"
|
||||
"-lwayland-client"
|
||||
"-Wl,--pop-state"
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
nativeBuildInputs = [
|
||||
pkgs.rustPlatform.bindgenHook
|
||||
pkgs.pkg-config
|
||||
pkgs.wrapGAppsHook4 # For `niri-visual-tests`
|
||||
];
|
||||
|
||||
buildInputs = niri.buildInputs ++ [
|
||||
pkgs.libadwaita # For `niri-visual-tests`
|
||||
];
|
||||
|
||||
env = {
|
||||
# WARN: Do not overwrite this variable in your shell!
|
||||
# It is required for `dlopen()` to work on some libraries; see the comment
|
||||
# in the package expression
|
||||
#
|
||||
# This should only be set with `CARGO_BUILD_RUSTFLAGS="$CARGO_BUILD_RUSTFLAGS -C your-flags"`
|
||||
CARGO_BUILD_RUSTFLAGS = niri.RUSTFLAGS;
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style);
|
||||
|
||||
packages = forAllSystems (
|
||||
system:
|
||||
let
|
||||
niri = nixpkgsFor.${system}.callPackage niri-package { };
|
||||
in
|
||||
{
|
||||
inherit niri;
|
||||
|
||||
# NOTE: This is for development purposes only
|
||||
#
|
||||
# It is primarily to help with quickly iterating on
|
||||
# changes made to the above expression - though it is
|
||||
# also not stripped in order to better debug niri itself
|
||||
niri-debug = niri.overrideAttrs (
|
||||
newAttrs: oldAttrs: {
|
||||
pname = oldAttrs.pname + "-debug";
|
||||
|
||||
cargoBuildType = "debug";
|
||||
cargoCheckType = newAttrs.cargoBuildType;
|
||||
|
||||
dontStrip = true;
|
||||
}
|
||||
);
|
||||
|
||||
default = niri;
|
||||
}
|
||||
);
|
||||
|
||||
overlays.default = final: _: {
|
||||
niri = final.callPackage niri-package { };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,11 +9,15 @@ repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
csscolorparser = "0.6.2"
|
||||
csscolorparser = "0.7.0"
|
||||
knuffel = "3.2.0"
|
||||
miette = "5.10.0"
|
||||
niri-ipc = { version = "0.1.6", path = "../niri-ipc" }
|
||||
regex = "1.10.4"
|
||||
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
|
||||
niri-ipc = { version = "25.5.1", path = "../niri-ipc" }
|
||||
regex = "1.11.1"
|
||||
smithay = { workspace = true, features = ["backend_libinput"] }
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
insta.workspace = true
|
||||
pretty_assertions = "1.4.1"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
use crate::{BlockOutFrom, CornerRadius, RegexEq, ShadowRule};
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
pub struct LayerRule {
|
||||
#[knuffel(children(name = "match"))]
|
||||
pub matches: Vec<Match>,
|
||||
#[knuffel(children(name = "exclude"))]
|
||||
pub excludes: Vec<Match>,
|
||||
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub opacity: Option<f32>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub block_out_from: Option<BlockOutFrom>,
|
||||
#[knuffel(child, default)]
|
||||
pub shadow: ShadowRule,
|
||||
#[knuffel(child)]
|
||||
pub geometry_corner_radius: Option<CornerRadius>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub place_within_backdrop: Option<bool>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub baba_is_float: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
pub struct Match {
|
||||
#[knuffel(property, str)]
|
||||
pub namespace: Option<RegexEq>,
|
||||
#[knuffel(property)]
|
||||
pub at_startup: Option<bool>,
|
||||
}
|
||||
+3269
-394
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
/// `Regex` that implements `PartialEq` by its string form.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RegexEq(pub Regex);
|
||||
|
||||
impl PartialEq for RegexEq {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.as_str() == other.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for RegexEq {}
|
||||
|
||||
impl FromStr for RegexEq {
|
||||
type Err = <Regex as FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Regex::from_str(s).map(Self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
struct KdlCodeBlock {
|
||||
filename: String,
|
||||
code: String,
|
||||
line_number: usize,
|
||||
must_fail: bool,
|
||||
}
|
||||
|
||||
fn extract_kdl_from_file(file_contents: &str, filename: &str) -> Vec<KdlCodeBlock> {
|
||||
let mut lines = file_contents
|
||||
.lines()
|
||||
.map(|line| {
|
||||
// Removes the > from callouts that might contain ```kdl```
|
||||
let line = line.trim();
|
||||
if line.starts_with('>') {
|
||||
if line.len() == 1 {
|
||||
""
|
||||
} else {
|
||||
&line[2..]
|
||||
}
|
||||
} else {
|
||||
line
|
||||
}
|
||||
})
|
||||
.enumerate();
|
||||
|
||||
let mut kdl_code_blocks = vec![];
|
||||
|
||||
while let Some((line_number, line)) = lines.next() {
|
||||
if !line.starts_with("```kdl") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut snippet = String::new();
|
||||
|
||||
for (_, line) in lines
|
||||
.by_ref()
|
||||
.take_while(|(_, line)| !line.starts_with("```"))
|
||||
{
|
||||
snippet.push_str(line);
|
||||
snippet.push('\n');
|
||||
}
|
||||
|
||||
kdl_code_blocks.push(KdlCodeBlock {
|
||||
code: snippet,
|
||||
line_number,
|
||||
filename: filename.to_string(),
|
||||
must_fail: line.contains("must-fail"),
|
||||
});
|
||||
}
|
||||
|
||||
kdl_code_blocks
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wiki_docs_parses() {
|
||||
let wiki_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../wiki");
|
||||
|
||||
let code_blocks = fs::read_dir(wiki_dir)
|
||||
.unwrap()
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.file_type().is_ok_and(|ft| ft.is_file()))
|
||||
.filter(|file| {
|
||||
file.path()
|
||||
.extension()
|
||||
.map(|ext| ext == "md")
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.flat_map(|file| {
|
||||
let file_contents = fs::read_to_string(file.path()).unwrap();
|
||||
let file_path = file.path();
|
||||
let filename = file_path.to_str().unwrap();
|
||||
extract_kdl_from_file(&file_contents, filename)
|
||||
});
|
||||
|
||||
let mut errors = vec![];
|
||||
|
||||
for KdlCodeBlock {
|
||||
code,
|
||||
line_number,
|
||||
filename,
|
||||
must_fail,
|
||||
} in code_blocks
|
||||
{
|
||||
if let Err(error) = niri_config::Config::parse(&filename, &code) {
|
||||
if !must_fail {
|
||||
errors.push(format!(
|
||||
"Error parsing wiki KDL code block at {}:{}: {:?}",
|
||||
filename,
|
||||
line_number,
|
||||
miette::Report::new(error)
|
||||
));
|
||||
}
|
||||
} else if must_fail {
|
||||
errors.push(format!(
|
||||
"Expected error parsing wiki KDL code block at {}:{}",
|
||||
filename, line_number
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
panic!(
|
||||
"Errors parsing {} wiki KDL code blocks:\n{}",
|
||||
errors.len(),
|
||||
errors.join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -1,16 +1,22 @@
|
||||
[package]
|
||||
name = "niri-ipc"
|
||||
version.workspace = true
|
||||
description.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
description = "Types and helpers for interfacing with the niri Wayland compositor."
|
||||
keywords = ["wayland"]
|
||||
categories = ["api-bindings", "os"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true, optional = true }
|
||||
schemars = { version = "0.8.22", optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[features]
|
||||
clap = ["dep:clap"]
|
||||
json-schema = ["dep:schemars"]
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# niri-ipc
|
||||
|
||||
Types and helpers for interfacing with the [niri](https://github.com/YaLTeR/niri) Wayland compositor.
|
||||
|
||||
## Backwards compatibility
|
||||
|
||||
This crate follows the niri version.
|
||||
It is **not** API-stable in terms of the Rust semver.
|
||||
In particular, expect new struct fields and enum variants to be added in patch version bumps.
|
||||
|
||||
Use an exact version requirement to avoid breaking changes:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
niri-ipc = "=25.5.1"
|
||||
```
|
||||
+980
-108
File diff suppressed because it is too large
Load Diff
+49
-11
@@ -1,12 +1,12 @@
|
||||
//! Helper for blocking communication over the niri socket.
|
||||
|
||||
use std::env;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::net::Shutdown;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{Reply, Request};
|
||||
use crate::{Event, Reply, Request};
|
||||
|
||||
/// Name of the environment variable containing the niri IPC socket path.
|
||||
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
|
||||
@@ -16,7 +16,7 @@ pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
|
||||
/// This struct is used to communicate with the niri IPC server. It handles the socket connection
|
||||
/// and serialization/deserialization of messages.
|
||||
pub struct Socket {
|
||||
stream: UnixStream,
|
||||
stream: BufReader<UnixStream>,
|
||||
}
|
||||
|
||||
impl Socket {
|
||||
@@ -37,6 +37,7 @@ impl Socket {
|
||||
/// Connects to the niri IPC socket at the given path.
|
||||
pub fn connect_to(path: impl AsRef<Path>) -> io::Result<Self> {
|
||||
let stream = UnixStream::connect(path.as_ref())?;
|
||||
let stream = BufReader::new(stream);
|
||||
Ok(Self { stream })
|
||||
}
|
||||
|
||||
@@ -47,17 +48,54 @@ impl Socket {
|
||||
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
|
||||
/// * `Ok(Err(message))`: error message from niri
|
||||
/// * `Err(error)`: error communicating with niri
|
||||
pub fn send(self, request: Request) -> io::Result<Reply> {
|
||||
let Self { mut stream } = self;
|
||||
|
||||
let mut buf = serde_json::to_vec(&request).unwrap();
|
||||
stream.write_all(&buf)?;
|
||||
stream.shutdown(Shutdown::Write)?;
|
||||
pub fn send(&mut self, request: Request) -> io::Result<Reply> {
|
||||
let mut buf = serde_json::to_string(&request).unwrap();
|
||||
buf.push('\n');
|
||||
self.stream.get_mut().write_all(buf.as_bytes())?;
|
||||
|
||||
buf.clear();
|
||||
stream.read_to_end(&mut buf)?;
|
||||
self.stream.read_line(&mut buf)?;
|
||||
|
||||
let reply = serde_json::from_slice(&buf)?;
|
||||
let reply = serde_json::from_str(&buf)?;
|
||||
Ok(reply)
|
||||
}
|
||||
|
||||
/// Starts reading event stream [`Event`]s from the socket.
|
||||
///
|
||||
/// The returned function will block until the next [`Event`] arrives, then return it.
|
||||
///
|
||||
/// Use this only after requesting an [`EventStream`][Request::EventStream].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use niri_ipc::{Request, Response};
|
||||
/// use niri_ipc::socket::Socket;
|
||||
///
|
||||
/// fn main() -> std::io::Result<()> {
|
||||
/// let mut socket = Socket::connect()?;
|
||||
///
|
||||
/// let reply = socket.send(Request::EventStream)?;
|
||||
/// if matches!(reply, Ok(Response::Handled)) {
|
||||
/// let mut read_event = socket.read_events();
|
||||
/// while let Ok(event) = read_event() {
|
||||
/// println!("Received event: {event:?}");
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn read_events(self) -> impl FnMut() -> io::Result<Event> {
|
||||
let Self { mut stream } = self;
|
||||
let _ = stream.get_mut().shutdown(Shutdown::Write);
|
||||
|
||||
let mut buf = String::new();
|
||||
move || {
|
||||
buf.clear();
|
||||
stream.read_line(&mut buf)?;
|
||||
let event = serde_json::from_str(&buf)?;
|
||||
Ok(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
//! Helpers for keeping track of the event stream state.
|
||||
//!
|
||||
//! 1. Create an [`EventStreamState`] using `Default::default()`, or any individual state part if
|
||||
//! you only care about part of the state.
|
||||
//! 2. Connect to the niri socket and request an event stream.
|
||||
//! 3. Pass every [`Event`] to [`EventStreamStatePart::apply`] on your state.
|
||||
//! 4. Read the fields of the state as needed.
|
||||
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{Event, KeyboardLayouts, Window, Workspace};
|
||||
|
||||
/// Part of the state communicated via the event stream.
|
||||
pub trait EventStreamStatePart {
|
||||
/// Returns a sequence of events that replicates this state from default initialization.
|
||||
fn replicate(&self) -> Vec<Event>;
|
||||
|
||||
/// Applies the event to this state.
|
||||
///
|
||||
/// Returns `None` after applying the event, and `Some(event)` if the event is ignored by this
|
||||
/// part of the state.
|
||||
fn apply(&mut self, event: Event) -> Option<Event>;
|
||||
}
|
||||
|
||||
/// The full state communicated over the event stream.
|
||||
///
|
||||
/// Different parts of the state are not guaranteed to be consistent across every single event
|
||||
/// sent by niri. For example, you may receive the first [`Event::WindowOpenedOrChanged`] for a
|
||||
/// just-opened window *after* an [`Event::WorkspaceActiveWindowChanged`] for that window. Between
|
||||
/// these two events, the workspace active window id refers to a window that does not yet exist in
|
||||
/// the windows state part.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EventStreamState {
|
||||
/// State of workspaces.
|
||||
pub workspaces: WorkspacesState,
|
||||
|
||||
/// State of workspaces.
|
||||
pub windows: WindowsState,
|
||||
|
||||
/// State of the keyboard layouts.
|
||||
pub keyboard_layouts: KeyboardLayoutsState,
|
||||
|
||||
/// State of the overview.
|
||||
pub overview: OverviewState,
|
||||
}
|
||||
|
||||
/// The workspaces state communicated over the event stream.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WorkspacesState {
|
||||
/// Map from a workspace id to the workspace.
|
||||
pub workspaces: HashMap<u64, Workspace>,
|
||||
}
|
||||
|
||||
/// The windows state communicated over the event stream.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WindowsState {
|
||||
/// Map from a window id to the window.
|
||||
pub windows: HashMap<u64, Window>,
|
||||
}
|
||||
|
||||
/// The keyboard layout state communicated over the event stream.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct KeyboardLayoutsState {
|
||||
/// Configured keyboard layouts.
|
||||
pub keyboard_layouts: Option<KeyboardLayouts>,
|
||||
}
|
||||
|
||||
/// The overview state communicated over the event stream.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct OverviewState {
|
||||
/// Whether the overview is currently open.
|
||||
pub is_open: bool,
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for EventStreamState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let mut events = Vec::new();
|
||||
events.extend(self.workspaces.replicate());
|
||||
events.extend(self.windows.replicate());
|
||||
events.extend(self.keyboard_layouts.replicate());
|
||||
events.extend(self.overview.replicate());
|
||||
events
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
let event = self.workspaces.apply(event)?;
|
||||
let event = self.windows.apply(event)?;
|
||||
let event = self.keyboard_layouts.apply(event)?;
|
||||
let event = self.overview.apply(event)?;
|
||||
Some(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for WorkspacesState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let workspaces = self.workspaces.values().cloned().collect();
|
||||
vec![Event::WorkspacesChanged { workspaces }]
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
match event {
|
||||
Event::WorkspacesChanged { workspaces } => {
|
||||
self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect();
|
||||
}
|
||||
Event::WorkspaceUrgencyChanged { id, urgent } => {
|
||||
for ws in self.workspaces.values_mut() {
|
||||
if ws.id == id {
|
||||
ws.is_urgent = urgent;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::WorkspaceActivated { id, focused } => {
|
||||
let ws = self.workspaces.get(&id);
|
||||
let ws = ws.expect("activated workspace was missing from the map");
|
||||
let output = ws.output.clone();
|
||||
|
||||
for ws in self.workspaces.values_mut() {
|
||||
let got_activated = ws.id == id;
|
||||
if ws.output == output {
|
||||
ws.is_active = got_activated;
|
||||
}
|
||||
|
||||
if focused {
|
||||
ws.is_focused = got_activated;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::WorkspaceActiveWindowChanged {
|
||||
workspace_id,
|
||||
active_window_id,
|
||||
} => {
|
||||
let ws = self.workspaces.get_mut(&workspace_id);
|
||||
let ws = ws.expect("changed workspace was missing from the map");
|
||||
ws.active_window_id = active_window_id;
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for WindowsState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let windows = self.windows.values().cloned().collect();
|
||||
vec![Event::WindowsChanged { windows }]
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
match event {
|
||||
Event::WindowsChanged { windows } => {
|
||||
self.windows = windows.into_iter().map(|win| (win.id, win)).collect();
|
||||
}
|
||||
Event::WindowOpenedOrChanged { window } => {
|
||||
let (id, is_focused) = match self.windows.entry(window.id) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
let entry = entry.get_mut();
|
||||
*entry = window;
|
||||
(entry.id, entry.is_focused)
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let entry = entry.insert(window);
|
||||
(entry.id, entry.is_focused)
|
||||
}
|
||||
};
|
||||
|
||||
if is_focused {
|
||||
for win in self.windows.values_mut() {
|
||||
if win.id != id {
|
||||
win.is_focused = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::WindowClosed { id } => {
|
||||
let win = self.windows.remove(&id);
|
||||
win.expect("closed window was missing from the map");
|
||||
}
|
||||
Event::WindowFocusChanged { id } => {
|
||||
for win in self.windows.values_mut() {
|
||||
win.is_focused = Some(win.id) == id;
|
||||
}
|
||||
}
|
||||
Event::WindowUrgencyChanged { id, urgent } => {
|
||||
for win in self.windows.values_mut() {
|
||||
if win.id == id {
|
||||
win.is_urgent = urgent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for KeyboardLayoutsState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
if let Some(keyboard_layouts) = self.keyboard_layouts.clone() {
|
||||
vec![Event::KeyboardLayoutsChanged { keyboard_layouts }]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
match event {
|
||||
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
|
||||
self.keyboard_layouts = Some(keyboard_layouts);
|
||||
}
|
||||
Event::KeyboardLayoutSwitched { idx } => {
|
||||
let kb = self.keyboard_layouts.as_mut();
|
||||
let kb = kb.expect("keyboard layouts must be set before a layout can be switched");
|
||||
kb.current_idx = idx;
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for OverviewState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
vec![Event::OverviewOpenedOrClosed {
|
||||
is_open: self.is_open,
|
||||
}]
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
match event {
|
||||
Event::OverviewOpenedOrClosed { is_open } => {
|
||||
self.is_open = is_open;
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,11 @@ edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
adw = { version = "0.6.0", package = "libadwaita", features = ["v1_4"] }
|
||||
adw = { version = "0.7.2", package = "libadwaita", features = ["v1_4"] }
|
||||
anyhow.workspace = true
|
||||
gtk = { version = "0.8.2", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "0.1.6", path = ".." }
|
||||
niri-config = { version = "0.1.6", path = "../niri-config" }
|
||||
gtk = { version = "0.9.6", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "25.5.1", path = ".." }
|
||||
niri-config = { version = "25.5.1", path = "../niri-config" }
|
||||
smithay.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
use std::f32::consts::{FRAC_PI_2, PI};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::animation::ANIMATION_SLOWDOWN;
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::CornerRadius;
|
||||
use niri_config::{Color, CornerRadius, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientAngle {
|
||||
angle: f32,
|
||||
@@ -17,7 +15,7 @@ pub struct GradientAngle {
|
||||
}
|
||||
|
||||
impl GradientAngle {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
angle: 0.,
|
||||
prev_time: Duration::ZERO,
|
||||
@@ -31,20 +29,13 @@ impl TestCase for GradientAngle {
|
||||
}
|
||||
|
||||
fn advance_animations(&mut self, current_time: Duration) {
|
||||
let mut delta = if self.prev_time.is_zero() {
|
||||
let delta = if self.prev_time.is_zero() {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
current_time.saturating_sub(self.prev_time)
|
||||
};
|
||||
self.prev_time = current_time;
|
||||
|
||||
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
|
||||
if slowdown == 0. {
|
||||
delta = Duration::ZERO
|
||||
} else {
|
||||
delta = delta.div_f64(slowdown);
|
||||
}
|
||||
|
||||
self.angle += delta.as_secs_f32() * PI;
|
||||
|
||||
if self.angle >= PI * 2. {
|
||||
@@ -59,17 +50,20 @@ impl TestCase for GradientAngle {
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 4, size.h / 4);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), size);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0, 0), area.size),
|
||||
[1., 0., 0., 1.],
|
||||
[0., 1., 0., 1.],
|
||||
Rectangle::from_size(area.size),
|
||||
GradientInterpolation::default(),
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
self.angle - FRAC_PI_2,
|
||||
Rectangle::from_loc_and_size((0, 0), area.size),
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
use std::f32::consts::{FRAC_PI_4, PI};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::animation::ANIMATION_SLOWDOWN;
|
||||
use niri::layout::focus_ring::FocusRing;
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{Color, CornerRadius};
|
||||
use niri_config::{Color, CornerRadius, FloatOrInt, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientArea {
|
||||
progress: f32,
|
||||
@@ -19,14 +17,16 @@ pub struct GradientArea {
|
||||
}
|
||||
|
||||
impl GradientArea {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
let border = FocusRing::new(niri_config::FocusRing {
|
||||
off: false,
|
||||
width: 1,
|
||||
active_color: Color::new(255, 255, 255, 128),
|
||||
width: FloatOrInt(1.),
|
||||
active_color: Color::from_rgba8_unpremul(255, 255, 255, 128),
|
||||
inactive_color: Color::default(),
|
||||
urgent_color: Color::default(),
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -43,20 +43,13 @@ impl TestCase for GradientArea {
|
||||
}
|
||||
|
||||
fn advance_animations(&mut self, current_time: Duration) {
|
||||
let mut delta = if self.prev_time.is_zero() {
|
||||
let delta = if self.prev_time.is_zero() {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
current_time.saturating_sub(self.prev_time)
|
||||
};
|
||||
self.prev_time = current_time;
|
||||
|
||||
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
|
||||
if slowdown == 0. {
|
||||
delta = Duration::ZERO
|
||||
} else {
|
||||
delta = delta.div_f64(slowdown);
|
||||
}
|
||||
|
||||
self.progress += delta.as_secs_f32() * PI;
|
||||
|
||||
if self.progress >= PI * 2. {
|
||||
@@ -74,27 +67,31 @@ impl TestCase for GradientArea {
|
||||
let f = (self.progress.sin() + 1.) / 2.;
|
||||
|
||||
let (a, b) = (size.w / 4, size.h / 4);
|
||||
let rect_size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), rect_size);
|
||||
let rect_size = Size::from((size.w - a * 2, size.h - b * 2));
|
||||
let area = Rectangle::new(Point::from((a, b)), rect_size).to_f64();
|
||||
|
||||
let g_size = Size::from((
|
||||
(size.w as f32 / 8. + size.w as f32 / 8. * 7. * f).round() as i32,
|
||||
(size.h as f32 / 8. + size.h as f32 / 8. * 7. * f).round() as i32,
|
||||
));
|
||||
let g_loc = ((size.w - g_size.w) / 2, (size.h - g_size.h) / 2);
|
||||
let mut g_area = Rectangle::from_loc_and_size(g_loc, g_size);
|
||||
let g_loc = Point::from(((size.w - g_size.w) / 2, (size.h - g_size.h) / 2)).to_f64();
|
||||
let g_size = g_size.to_f64();
|
||||
let mut g_area = Rectangle::new(g_loc, g_size);
|
||||
g_area.loc -= area.loc;
|
||||
|
||||
self.border.update_render_elements(
|
||||
g_size,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
Rectangle::default(),
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
);
|
||||
rv.extend(
|
||||
self.border
|
||||
.render(renderer, Point::from(g_loc), Scale::from(1.))
|
||||
.render(renderer, g_loc)
|
||||
.map(|elem| Box::new(elem) as _),
|
||||
);
|
||||
|
||||
@@ -102,12 +99,15 @@ impl TestCase for GradientArea {
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
g_area,
|
||||
[1., 0., 0., 1.],
|
||||
[0., 1., 0., 1.],
|
||||
GradientInterpolation::default(),
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
FRAC_PI_4,
|
||||
Rectangle::from_loc_and_size((0, 0), rect_size),
|
||||
Rectangle::from_size(rect_size).to_f64(),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklab {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklab {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklab,
|
||||
hue_interpolation: HueInterpolation::Shorter,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklab {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklabAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklabAlpha {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklab,
|
||||
hue_interpolation: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklabAlpha {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 0.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklchAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchAlpha {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
hue_interpolation: HueInterpolation::Longer,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklchAlpha {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 0.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklchDecreasing {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchDecreasing {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
hue_interpolation: HueInterpolation::Decreasing,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklchDecreasing {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklchIncreasing {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchIncreasing {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
hue_interpolation: HueInterpolation::Increasing,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklchIncreasing {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklchLonger {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchLonger {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
hue_interpolation: HueInterpolation::Longer,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklchLonger {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientOklchShorter {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchShorter {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
hue_interpolation: HueInterpolation::Shorter,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklchShorter {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientSrgb {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgb {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Srgb,
|
||||
hue_interpolation: HueInterpolation::Shorter,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientSrgb {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientSrgbAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgbAlpha {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Srgb,
|
||||
hue_interpolation: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientSrgbAlpha {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 0.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientSrgbLinear {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgbLinear {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::SrgbLinear,
|
||||
hue_interpolation: HueInterpolation::Shorter,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientSrgbLinear {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientSrgbLinearAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgbLinearAlpha {
|
||||
pub fn new(_args: Args) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::SrgbLinear,
|
||||
hue_interpolation: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientSrgbLinearAlpha {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_size(area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 0.),
|
||||
0.,
|
||||
Rectangle::from_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::layout::workspace::ColumnWidth;
|
||||
use niri::layout::{LayoutElement as _, Options};
|
||||
use niri::animation::Clock;
|
||||
use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options};
|
||||
use niri::render_helpers::RenderTarget;
|
||||
use niri::utils::get_monotonic_time;
|
||||
use niri_config::Color;
|
||||
use niri_config::{Color, FloatOrInt, OutputName, PresetSize};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::desktop::layer_map_for_output;
|
||||
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||
use smithay::utils::{Logical, Physical, Size};
|
||||
use smithay::utils::{Physical, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
use crate::test_window::TestWindow;
|
||||
|
||||
type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
|
||||
@@ -20,13 +19,16 @@ type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
|
||||
pub struct Layout {
|
||||
output: Output,
|
||||
windows: Vec<TestWindow>,
|
||||
clock: Clock,
|
||||
layout: niri::layout::Layout<TestWindow>,
|
||||
start_time: Duration,
|
||||
steps: HashMap<Duration, DynStepFn>,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn new(size: Size<i32, Logical>) -> Self {
|
||||
pub fn new(args: Args) -> Self {
|
||||
let Args { size, clock } = args;
|
||||
|
||||
let output = Output::new(
|
||||
String::new(),
|
||||
PhysicalProperties {
|
||||
@@ -41,6 +43,12 @@ impl Layout {
|
||||
refresh: 60000,
|
||||
});
|
||||
output.change_current_state(mode, None, None, None);
|
||||
output.user_data().insert_if_missing(|| OutputName {
|
||||
connector: String::new(),
|
||||
make: None,
|
||||
model: None,
|
||||
serial: None,
|
||||
});
|
||||
|
||||
let options = Options {
|
||||
focus_ring: niri_config::FocusRing {
|
||||
@@ -49,49 +57,54 @@ impl Layout {
|
||||
},
|
||||
border: niri_config::Border {
|
||||
off: false,
|
||||
width: 4,
|
||||
active_color: Color::new(255, 163, 72, 255),
|
||||
inactive_color: Color::new(50, 50, 50, 255),
|
||||
width: FloatOrInt(4.),
|
||||
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
|
||||
inactive_color: Color::from_rgba8_unpremul(50, 50, 50, 255),
|
||||
urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255),
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let mut layout = niri::layout::Layout::with_options(options);
|
||||
let mut layout = niri::layout::Layout::with_options(clock.clone(), options);
|
||||
layout.add_output(output.clone());
|
||||
|
||||
let start_time = clock.now_unadjusted();
|
||||
|
||||
Self {
|
||||
output,
|
||||
windows: Vec::new(),
|
||||
clock,
|
||||
layout,
|
||||
start_time: get_monotonic_time(),
|
||||
start_time,
|
||||
steps: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_in_between(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
pub fn open_in_between(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.3)));
|
||||
rv.layout.activate_window(&0);
|
||||
|
||||
rv.add_step(500, |l| {
|
||||
let win = TestWindow::freeform(2);
|
||||
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
|
||||
l.add_window(win.clone(), Some(PresetSize::Proportion(0.3)));
|
||||
l.layout.start_open_animation_for_window(win.id());
|
||||
});
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_multiple_quickly(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
pub fn open_multiple_quickly(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
for delay in [100, 200, 300] {
|
||||
rv.add_step(delay, move |l| {
|
||||
let win = TestWindow::freeform(delay as usize);
|
||||
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
|
||||
l.add_window(win.clone(), Some(PresetSize::Proportion(0.3)));
|
||||
l.layout.start_open_animation_for_window(win.id());
|
||||
});
|
||||
}
|
||||
@@ -99,13 +112,13 @@ impl Layout {
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_multiple_quickly_big(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
pub fn open_multiple_quickly_big(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
for delay in [100, 200, 300] {
|
||||
rv.add_step(delay, move |l| {
|
||||
let win = TestWindow::freeform(delay as usize);
|
||||
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.5)));
|
||||
l.add_window(win.clone(), Some(PresetSize::Proportion(0.5)));
|
||||
l.layout.start_open_animation_for_window(win.id());
|
||||
});
|
||||
}
|
||||
@@ -113,44 +126,59 @@ impl Layout {
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_to_the_left(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
pub fn open_to_the_left(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.3)));
|
||||
|
||||
rv.add_step(500, |l| {
|
||||
let win = TestWindow::freeform(2);
|
||||
let right_of = l.windows[0].clone();
|
||||
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.3)));
|
||||
l.add_window_right_of(&right_of, win.clone(), Some(PresetSize::Proportion(0.3)));
|
||||
l.layout.start_open_animation_for_window(win.id());
|
||||
});
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn open_to_the_left_big(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::new(size);
|
||||
pub fn open_to_the_left_big(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.8)));
|
||||
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.8)));
|
||||
|
||||
rv.add_step(500, |l| {
|
||||
let win = TestWindow::freeform(2);
|
||||
let right_of = l.windows[0].clone();
|
||||
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.5)));
|
||||
l.add_window_right_of(&right_of, win.clone(), Some(PresetSize::Proportion(0.5)));
|
||||
l.layout.start_open_animation_for_window(win.id());
|
||||
});
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) {
|
||||
fn add_window(&mut self, mut window: TestWindow, width: Option<PresetSize>) {
|
||||
let ws = self.layout.active_workspace().unwrap();
|
||||
window.request_size(ws.new_window_size(width, window.rules()), false);
|
||||
let min_size = window.min_size();
|
||||
let max_size = window.max_size();
|
||||
window.request_size(
|
||||
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
window.communicate();
|
||||
|
||||
self.layout.add_window(window.clone(), width, false);
|
||||
self.layout.add_window(
|
||||
window.clone(),
|
||||
AddWindowTarget::Auto,
|
||||
width,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
ActivateWindow::default(),
|
||||
);
|
||||
self.windows.push(window);
|
||||
}
|
||||
|
||||
@@ -158,14 +186,28 @@ impl Layout {
|
||||
&mut self,
|
||||
right_of: &TestWindow,
|
||||
mut window: TestWindow,
|
||||
width: Option<ColumnWidth>,
|
||||
width: Option<PresetSize>,
|
||||
) {
|
||||
let ws = self.layout.active_workspace().unwrap();
|
||||
window.request_size(ws.new_window_size(width, window.rules()), false);
|
||||
let min_size = window.min_size();
|
||||
let max_size = window.max_size();
|
||||
window.request_size(
|
||||
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
window.communicate();
|
||||
|
||||
self.layout
|
||||
.add_window_right_of(right_of.id(), window.clone(), width, false);
|
||||
self.layout.add_window(
|
||||
window.clone(),
|
||||
AddWindowTarget::NextTo(right_of.id()),
|
||||
width,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
ActivateWindow::default(),
|
||||
);
|
||||
self.windows.push(window);
|
||||
}
|
||||
|
||||
@@ -192,29 +234,28 @@ impl TestCase for Layout {
|
||||
}
|
||||
|
||||
fn are_animations_ongoing(&self) -> bool {
|
||||
self.layout
|
||||
.monitor_for_output(&self.output)
|
||||
.unwrap()
|
||||
.are_animations_ongoing()
|
||||
|| !self.steps.is_empty()
|
||||
self.layout.are_animations_ongoing(Some(&self.output)) || !self.steps.is_empty()
|
||||
}
|
||||
|
||||
fn advance_animations(&mut self, mut current_time: Duration) {
|
||||
fn advance_animations(&mut self, _current_time: Duration) {
|
||||
let now_unadjusted = self.clock.now_unadjusted();
|
||||
let run = self
|
||||
.steps
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|delay| self.start_time + *delay <= current_time)
|
||||
.filter(|delay| self.start_time + *delay <= now_unadjusted)
|
||||
.collect::<Vec<_>>();
|
||||
for key in &run {
|
||||
let f = self.steps.remove(key).unwrap();
|
||||
for delay in &run {
|
||||
let now = self.start_time + *delay;
|
||||
self.clock.set_unadjusted(now);
|
||||
self.layout.advance_animations();
|
||||
|
||||
let f = self.steps.remove(delay).unwrap();
|
||||
f(self);
|
||||
}
|
||||
if !run.is_empty() {
|
||||
current_time = get_monotonic_time();
|
||||
}
|
||||
|
||||
self.layout.advance_animations(current_time);
|
||||
self.clock.set_unadjusted(now_unadjusted);
|
||||
self.layout.advance_animations();
|
||||
}
|
||||
|
||||
fn render(
|
||||
@@ -222,12 +263,12 @@ impl TestCase for Layout {
|
||||
renderer: &mut GlesRenderer,
|
||||
_size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
self.layout.update_render_elements(&self.output);
|
||||
self.layout.update_render_elements(Some(&self.output));
|
||||
self.layout
|
||||
.monitor_for_output(&self.output)
|
||||
.unwrap()
|
||||
.render_elements(renderer, RenderTarget::Output)
|
||||
.into_iter()
|
||||
.render_elements(renderer, RenderTarget::Output, true)
|
||||
.flat_map(|(_, iter)| iter)
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::animation::Clock;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Size};
|
||||
use smithay::utils::{Logical, Physical, Size};
|
||||
|
||||
pub mod gradient_angle;
|
||||
pub mod gradient_area;
|
||||
pub mod gradient_oklab;
|
||||
pub mod gradient_oklab_alpha;
|
||||
pub mod gradient_oklch_alpha;
|
||||
pub mod gradient_oklch_decreasing;
|
||||
pub mod gradient_oklch_increasing;
|
||||
pub mod gradient_oklch_longer;
|
||||
pub mod gradient_oklch_shorter;
|
||||
pub mod gradient_srgb;
|
||||
pub mod gradient_srgb_alpha;
|
||||
pub mod gradient_srgblinear;
|
||||
pub mod gradient_srgblinear_alpha;
|
||||
pub mod layout;
|
||||
pub mod tile;
|
||||
pub mod window;
|
||||
|
||||
pub struct Args {
|
||||
pub size: Size<i32, Logical>,
|
||||
pub clock: Clock,
|
||||
}
|
||||
|
||||
pub trait TestCase {
|
||||
fn resize(&mut self, _width: i32, _height: i32) {}
|
||||
fn are_animations_ongoing(&self) -> bool {
|
||||
|
||||
@@ -3,12 +3,12 @@ use std::time::Duration;
|
||||
|
||||
use niri::layout::Options;
|
||||
use niri::render_helpers::RenderTarget;
|
||||
use niri_config::Color;
|
||||
use niri_config::{Color, FloatOrInt};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
use crate::test_window::TestWindow;
|
||||
|
||||
pub struct Tile {
|
||||
@@ -17,53 +17,46 @@ pub struct Tile {
|
||||
}
|
||||
|
||||
impl Tile {
|
||||
pub fn freeform(size: Size<i32, Logical>) -> Self {
|
||||
pub fn freeform(args: Args) -> Self {
|
||||
let window = TestWindow::freeform(0);
|
||||
let mut rv = Self::with_window(window);
|
||||
rv.tile.request_tile_size(size, false);
|
||||
rv.window.communicate();
|
||||
rv
|
||||
Self::with_window(args, window)
|
||||
}
|
||||
|
||||
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
|
||||
pub fn fixed_size(args: Args) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
let mut rv = Self::with_window(window);
|
||||
rv.tile.request_tile_size(size, false);
|
||||
rv.window.communicate();
|
||||
rv
|
||||
Self::with_window(args, window)
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
|
||||
pub fn fixed_size_with_csd_shadow(args: Args) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
window.set_csd_shadow_width(64);
|
||||
let mut rv = Self::with_window(window);
|
||||
rv.tile.request_tile_size(size, false);
|
||||
rv.window.communicate();
|
||||
rv
|
||||
Self::with_window(args, window)
|
||||
}
|
||||
|
||||
pub fn freeform_open(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::freeform(size);
|
||||
pub fn freeform_open(args: Args) -> Self {
|
||||
let mut rv = Self::freeform(args);
|
||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||
rv.tile.start_open_animation();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn fixed_size_open(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::fixed_size(size);
|
||||
pub fn fixed_size_open(args: Args) -> Self {
|
||||
let mut rv = Self::fixed_size(args);
|
||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||
rv.tile.start_open_animation();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow_open(size: Size<i32, Logical>) -> Self {
|
||||
let mut rv = Self::fixed_size_with_csd_shadow(size);
|
||||
pub fn fixed_size_with_csd_shadow_open(args: Args) -> Self {
|
||||
let mut rv = Self::fixed_size_with_csd_shadow(args);
|
||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||
rv.tile.start_open_animation();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn with_window(window: TestWindow) -> Self {
|
||||
pub fn with_window(args: Args, window: TestWindow) -> Self {
|
||||
let Args { size, clock } = args;
|
||||
|
||||
let options = Options {
|
||||
focus_ring: niri_config::FocusRing {
|
||||
off: true,
|
||||
@@ -71,21 +64,34 @@ impl Tile {
|
||||
},
|
||||
border: niri_config::Border {
|
||||
off: false,
|
||||
width: 32,
|
||||
active_color: Color::new(255, 163, 72, 255),
|
||||
width: FloatOrInt(32.),
|
||||
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let tile = niri::layout::tile::Tile::new(window.clone(), Rc::new(options));
|
||||
|
||||
let mut tile = niri::layout::tile::Tile::new(
|
||||
window.clone(),
|
||||
size.to_f64(),
|
||||
1.,
|
||||
clock,
|
||||
Rc::new(options),
|
||||
);
|
||||
|
||||
tile.request_tile_size(size.to_f64(), false, None);
|
||||
window.communicate();
|
||||
|
||||
Self { window, tile }
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for Tile {
|
||||
fn resize(&mut self, width: i32, height: i32) {
|
||||
let size = Size::from((width, height)).to_f64();
|
||||
self.tile
|
||||
.request_tile_size(Size::from((width, height)), false);
|
||||
.update_config(size, 1., self.tile.options().clone());
|
||||
self.tile.request_tile_size(size, false, None);
|
||||
self.window.communicate();
|
||||
}
|
||||
|
||||
@@ -93,8 +99,8 @@ impl TestCase for Tile {
|
||||
self.tile.are_animations_ongoing()
|
||||
}
|
||||
|
||||
fn advance_animations(&mut self, current_time: Duration) {
|
||||
self.tile.advance_animations(current_time);
|
||||
fn advance_animations(&mut self, _current_time: Duration) {
|
||||
self.tile.advance_animations();
|
||||
}
|
||||
|
||||
fn render(
|
||||
@@ -102,21 +108,16 @@ impl TestCase for Tile {
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let tile_size = self.tile.tile_size().to_physical(1);
|
||||
let location = Point::from(((size.w - tile_size.w) / 2, (size.h - tile_size.h) / 2));
|
||||
let size = size.to_f64();
|
||||
let tile_size = self.tile.tile_size().to_physical(1.);
|
||||
let location = Point::from((size.w - tile_size.w, size.h - tile_size.h)).downscale(2.);
|
||||
|
||||
self.tile.update(
|
||||
self.tile.update_render_elements(
|
||||
true,
|
||||
Rectangle::from_loc_and_size((-location.x, -location.y), size.to_logical(1)),
|
||||
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
|
||||
);
|
||||
self.tile
|
||||
.render(
|
||||
renderer,
|
||||
location,
|
||||
Scale::from(1.),
|
||||
true,
|
||||
RenderTarget::Output,
|
||||
)
|
||||
.render(renderer, location, true, RenderTarget::Output)
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ use niri::layout::LayoutElement;
|
||||
use niri::render_helpers::RenderTarget;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Point, Scale, Size};
|
||||
use smithay::utils::{Physical, Point, Scale, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
use crate::test_window::TestWindow;
|
||||
|
||||
pub struct Window {
|
||||
@@ -12,24 +12,24 @@ pub struct Window {
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn freeform(size: Size<i32, Logical>) -> Self {
|
||||
pub fn freeform(args: Args) -> Self {
|
||||
let mut window = TestWindow::freeform(0);
|
||||
window.request_size(size, false);
|
||||
window.request_size(args.size, false, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
|
||||
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
|
||||
pub fn fixed_size(args: Args) -> Self {
|
||||
let mut window = TestWindow::fixed_size(0);
|
||||
window.request_size(size, false);
|
||||
window.request_size(args.size, false, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
|
||||
pub fn fixed_size_with_csd_shadow(args: Args) -> Self {
|
||||
let mut window = TestWindow::fixed_size(0);
|
||||
window.set_csd_shadow_width(64);
|
||||
window.request_size(size, false);
|
||||
window.request_size(args.size, false, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
@@ -37,7 +37,8 @@ impl Window {
|
||||
|
||||
impl TestCase for Window {
|
||||
fn resize(&mut self, width: i32, height: i32) {
|
||||
self.window.request_size(Size::from((width, height)), false);
|
||||
self.window
|
||||
.request_size(Size::from((width, height)), false, false, None);
|
||||
self.window.communicate();
|
||||
}
|
||||
|
||||
@@ -47,7 +48,9 @@ impl TestCase for Window {
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let win_size = self.window.size().to_physical(1);
|
||||
let location = Point::from(((size.w - win_size.w) / 2, (size.h - win_size.h) / 2));
|
||||
let location = Point::from((size.w - win_size.w, size.h - win_size.h))
|
||||
.to_f64()
|
||||
.downscale(2.);
|
||||
|
||||
self.window
|
||||
.render(
|
||||
|
||||
@@ -2,23 +2,30 @@
|
||||
extern crate tracing;
|
||||
|
||||
use std::env;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
|
||||
use cases::tile::Tile;
|
||||
use cases::window::Window;
|
||||
use gtk::prelude::{
|
||||
AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt,
|
||||
};
|
||||
use cases::Args;
|
||||
use gtk::prelude::{ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt};
|
||||
use gtk::{gdk, gio, glib};
|
||||
use niri::animation::ANIMATION_SLOWDOWN;
|
||||
use smithay::utils::{Logical, Size};
|
||||
use smithay_view::SmithayView;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::cases::gradient_angle::GradientAngle;
|
||||
use crate::cases::gradient_area::GradientArea;
|
||||
use crate::cases::gradient_oklab::GradientOklab;
|
||||
use crate::cases::gradient_oklab_alpha::GradientOklabAlpha;
|
||||
use crate::cases::gradient_oklch_alpha::GradientOklchAlpha;
|
||||
use crate::cases::gradient_oklch_decreasing::GradientOklchDecreasing;
|
||||
use crate::cases::gradient_oklch_increasing::GradientOklchIncreasing;
|
||||
use crate::cases::gradient_oklch_longer::GradientOklchLonger;
|
||||
use crate::cases::gradient_oklch_shorter::GradientOklchShorter;
|
||||
use crate::cases::gradient_srgb::GradientSrgb;
|
||||
use crate::cases::gradient_srgb_alpha::GradientSrgbAlpha;
|
||||
use crate::cases::gradient_srgblinear::GradientSrgbLinear;
|
||||
use crate::cases::gradient_srgblinear_alpha::GradientSrgbLinearAlpha;
|
||||
use crate::cases::layout::Layout;
|
||||
use crate::cases::tile::Tile;
|
||||
use crate::cases::window::Window;
|
||||
use crate::cases::TestCase;
|
||||
|
||||
mod cases;
|
||||
@@ -55,24 +62,23 @@ fn on_startup(_app: &adw::Application) {
|
||||
|
||||
fn build_ui(app: &adw::Application) {
|
||||
let stack = gtk::Stack::new();
|
||||
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
|
||||
|
||||
struct S {
|
||||
stack: gtk::Stack,
|
||||
anim_adjustment: gtk::Adjustment,
|
||||
}
|
||||
|
||||
impl S {
|
||||
fn add<T: TestCase + 'static>(
|
||||
&self,
|
||||
make: impl Fn(Size<i32, Logical>) -> T + 'static,
|
||||
title: &str,
|
||||
) {
|
||||
let view = SmithayView::new(make);
|
||||
fn add<T: TestCase + 'static>(&self, make: impl Fn(Args) -> T + 'static, title: &str) {
|
||||
let view = SmithayView::new(make, &self.anim_adjustment);
|
||||
self.stack.add_titled(&view, None, title);
|
||||
}
|
||||
}
|
||||
|
||||
let s = S {
|
||||
stack: stack.clone(),
|
||||
anim_adjustment: anim_adjustment.clone(),
|
||||
};
|
||||
|
||||
s.add(Window::freeform, "Freeform Window");
|
||||
@@ -112,12 +118,20 @@ fn build_ui(app: &adw::Application) {
|
||||
|
||||
s.add(GradientAngle::new, "Gradient - Angle");
|
||||
s.add(GradientArea::new, "Gradient - Area");
|
||||
s.add(GradientSrgb::new, "Gradient - Srgb");
|
||||
s.add(GradientSrgbLinear::new, "Gradient - SrgbLinear");
|
||||
s.add(GradientOklab::new, "Gradient - Oklab");
|
||||
s.add(GradientOklchShorter::new, "Gradient - Oklch Shorter");
|
||||
s.add(GradientOklchLonger::new, "Gradient - Oklch Longer");
|
||||
s.add(GradientOklchIncreasing::new, "Gradient - Oklch Increasing");
|
||||
s.add(GradientOklchDecreasing::new, "Gradient - Oklch Decreasing");
|
||||
s.add(GradientSrgbAlpha::new, "Gradient - Srgb Alpha");
|
||||
s.add(GradientSrgbLinearAlpha::new, "Gradient - SrgbLinear Alpha");
|
||||
s.add(GradientOklabAlpha::new, "Gradient - Oklab Alpha");
|
||||
s.add(GradientOklchAlpha::new, "Gradient - Oklch Alpha");
|
||||
|
||||
let content_headerbar = adw::HeaderBar::new();
|
||||
|
||||
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
|
||||
anim_adjustment
|
||||
.connect_value_changed(|adj| ANIMATION_SLOWDOWN.store(adj.value(), Ordering::SeqCst));
|
||||
let anim_scale = gtk::Scale::new(gtk::Orientation::Horizontal, Some(&anim_adjustment));
|
||||
anim_scale.set_hexpand(true);
|
||||
|
||||
|
||||
@@ -1,35 +1,44 @@
|
||||
use gtk::glib;
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
use smithay::utils::{Logical, Size};
|
||||
use smithay::utils::Size;
|
||||
|
||||
use crate::cases::TestCase;
|
||||
use crate::cases::{Args, TestCase};
|
||||
|
||||
mod imp {
|
||||
use std::cell::{Cell, OnceCell, RefCell};
|
||||
use std::ptr::null;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{ensure, Context};
|
||||
use gtk::gdk;
|
||||
use gtk::prelude::*;
|
||||
use niri::animation::Clock;
|
||||
use niri::render_helpers::{resources, shaders};
|
||||
use niri::utils::get_monotonic_time;
|
||||
use smithay::backend::egl::ffi::egl;
|
||||
use smithay::backend::egl::EGLContext;
|
||||
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
|
||||
use smithay::backend::renderer::{Frame, Renderer, Unbind};
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::{Bind, Color32F, Frame, Offscreen, Renderer};
|
||||
use smithay::reexports::gbm::Format as Fourcc;
|
||||
use smithay::utils::{Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::*;
|
||||
|
||||
type DynMakeTestCase = Box<dyn Fn(Size<i32, Logical>) -> Box<dyn TestCase>>;
|
||||
type DynMakeTestCase = Box<dyn Fn(Args) -> Box<dyn TestCase>>;
|
||||
|
||||
struct RendererData {
|
||||
renderer: GlesRenderer,
|
||||
dummy_texture: GlesTexture,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SmithayView {
|
||||
gl_area: gtk::GLArea,
|
||||
size: Cell<(i32, i32)>,
|
||||
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
|
||||
renderer: RefCell<Option<Result<RendererData, ()>>>,
|
||||
pub make_test_case: OnceCell<DynMakeTestCase>,
|
||||
test_case: RefCell<Option<Box<dyn TestCase>>>,
|
||||
pub clock: RefCell<Clock>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
@@ -122,32 +131,73 @@ mod imp {
|
||||
let Ok(renderer) = renderer else {
|
||||
return Ok(());
|
||||
};
|
||||
let RendererData {
|
||||
renderer,
|
||||
dummy_texture,
|
||||
} = renderer;
|
||||
|
||||
let size = self.size.get();
|
||||
|
||||
let frame_clock = self.obj().frame_clock().unwrap();
|
||||
let time = Duration::from_micros(frame_clock.frame_time() as u64);
|
||||
self.clock.borrow_mut().set_unadjusted(time);
|
||||
|
||||
// Create the test case if missing.
|
||||
let mut case = self.test_case.borrow_mut();
|
||||
let case = case.get_or_insert_with(|| {
|
||||
let make = self.make_test_case.get().unwrap();
|
||||
make(Size::from(size))
|
||||
let args = Args {
|
||||
size: Size::from(size),
|
||||
clock: self.clock.borrow().clone(),
|
||||
};
|
||||
make(args)
|
||||
});
|
||||
|
||||
case.advance_animations(get_monotonic_time());
|
||||
case.advance_animations(self.clock.borrow_mut().now());
|
||||
|
||||
let rect: Rectangle<i32, Physical> = Rectangle::from_loc_and_size((0, 0), size);
|
||||
let rect: Rectangle<i32, Physical> = Rectangle::from_size(Size::from(size));
|
||||
|
||||
let elements = unsafe {
|
||||
with_framebuffer_save_restore(renderer, |renderer| {
|
||||
case.render(renderer, Size::from(size))
|
||||
// Fetch GtkGLArea's framebuffer binding.
|
||||
let mut framebuffer = 0;
|
||||
renderer
|
||||
.with_context(|gl| unsafe {
|
||||
gl.GetIntegerv(
|
||||
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
|
||||
&mut framebuffer,
|
||||
);
|
||||
})
|
||||
}?;
|
||||
.context("error running closure in GL context")?;
|
||||
ensure!(framebuffer != 0, "error getting the framebuffer");
|
||||
|
||||
// This call will already change the framebuffer binding (offscreen elements will bind
|
||||
// intermediate textures during rendering).
|
||||
let elements = case.render(renderer, Size::from(size));
|
||||
|
||||
// HACK: there's currently no way to "just" render into an externally bound framebuffer
|
||||
// (like we have in this case). The render() call requires a valid target. So what
|
||||
// we'll do is use a dummy texture as a target, then swap the framebuffer binding right
|
||||
// before rendering.
|
||||
let mut dummy_target = renderer
|
||||
.bind(dummy_texture)
|
||||
.context("error binding dummy texture")?;
|
||||
|
||||
let mut frame = renderer
|
||||
.render(rect.size, Transform::Normal)
|
||||
.render(&mut dummy_target, rect.size, Transform::Normal)
|
||||
.context("error creating frame")?;
|
||||
|
||||
// Now that render() bound the dummy texture, change the binding underneath it back to
|
||||
// GtkGLArea's framebuffer, to render there instead.
|
||||
frame
|
||||
.clear([0.3, 0.3, 0.3, 1.], &[rect])
|
||||
.with_context(|gl| unsafe {
|
||||
gl.BindFramebuffer(
|
||||
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
|
||||
framebuffer as u32,
|
||||
);
|
||||
})
|
||||
.context("error running closure in GL context")?;
|
||||
|
||||
frame
|
||||
.clear(Color32F::from([0.3, 0.3, 0.3, 1.]), &[rect])
|
||||
.context("error clearing")?;
|
||||
|
||||
for element in elements.iter().rev() {
|
||||
@@ -157,7 +207,7 @@ mod imp {
|
||||
if let Some(mut damage) = rect.intersection(dst) {
|
||||
damage.loc -= dst.loc;
|
||||
element
|
||||
.draw(&mut frame, src, dst, &[damage])
|
||||
.draw(&mut frame, src, dst, &[damage], &[])
|
||||
.context("error drawing element")?;
|
||||
}
|
||||
}
|
||||
@@ -166,7 +216,7 @@ mod imp {
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_renderer() -> anyhow::Result<GlesRenderer> {
|
||||
unsafe fn create_renderer() -> anyhow::Result<RendererData> {
|
||||
smithay::backend::egl::ffi::make_sure_egl_is_loaded()
|
||||
.context("error loading EGL symbols in Smithay")?;
|
||||
|
||||
@@ -186,48 +236,20 @@ mod imp {
|
||||
|
||||
let egl_context = EGLContext::from_raw(egl_display, egl_config_id as *const _, egl_context)
|
||||
.context("error creating EGL context")?;
|
||||
let capabilities = GlesRenderer::supported_capabilities(&egl_context)
|
||||
.context("error getting supported renderer capabilities")?
|
||||
.into_iter()
|
||||
.filter(|c| *c != Capability::ColorTransformations);
|
||||
|
||||
let mut renderer = GlesRenderer::with_capabilities(egl_context, capabilities)
|
||||
.context("error creating GlesRenderer")?;
|
||||
let mut renderer = GlesRenderer::new(egl_context).context("error creating GlesRenderer")?;
|
||||
|
||||
let dummy_texture = renderer
|
||||
.create_buffer(Fourcc::Abgr8888, Size::from((1, 1)))
|
||||
.context("error creating dummy texture")?;
|
||||
|
||||
resources::init(&mut renderer);
|
||||
shaders::init(&mut renderer);
|
||||
|
||||
Ok(renderer)
|
||||
}
|
||||
|
||||
unsafe fn with_framebuffer_save_restore<T>(
|
||||
renderer: &mut GlesRenderer,
|
||||
f: impl FnOnce(&mut GlesRenderer) -> T,
|
||||
) -> anyhow::Result<T> {
|
||||
let mut framebuffer = 0;
|
||||
renderer
|
||||
.with_context(|gl| unsafe {
|
||||
gl.GetIntegerv(
|
||||
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
|
||||
&mut framebuffer,
|
||||
);
|
||||
})
|
||||
.context("error running closure in GL context")?;
|
||||
ensure!(framebuffer != 0, "error getting the framebuffer");
|
||||
|
||||
let rv = f(renderer);
|
||||
|
||||
renderer.unbind().context("error unbinding")?;
|
||||
renderer
|
||||
.with_context(|gl| unsafe {
|
||||
gl.BindFramebuffer(
|
||||
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
|
||||
framebuffer as u32,
|
||||
);
|
||||
})
|
||||
.context("error running closure in GL context")?;
|
||||
|
||||
Ok(rv)
|
||||
Ok(RendererData {
|
||||
renderer,
|
||||
dummy_texture,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,14 +260,32 @@ glib::wrapper! {
|
||||
|
||||
impl SmithayView {
|
||||
pub fn new<T: TestCase + 'static>(
|
||||
make_test_case: impl Fn(Size<i32, Logical>) -> T + 'static,
|
||||
make_test_case: impl Fn(Args) -> T + 'static,
|
||||
anim_adjustment: >k::Adjustment,
|
||||
) -> Self {
|
||||
let obj: Self = glib::Object::builder().build();
|
||||
|
||||
let make = move |size| Box::new(make_test_case(size)) as Box<dyn TestCase>;
|
||||
let make = move |args| Box::new(make_test_case(args)) as Box<dyn TestCase>;
|
||||
let make_test_case = Box::new(make) as _;
|
||||
let _ = obj.imp().make_test_case.set(make_test_case);
|
||||
|
||||
anim_adjustment.connect_value_changed({
|
||||
let obj = obj.downgrade();
|
||||
move |adj| {
|
||||
if let Some(obj) = obj.upgrade() {
|
||||
let mut clock = obj.imp().clock.borrow_mut();
|
||||
let instantly = adj.value() == 0.0;
|
||||
let rate = if instantly {
|
||||
1.0
|
||||
} else {
|
||||
1.0 / adj.value().max(0.001)
|
||||
};
|
||||
clock.set_rate(rate);
|
||||
clock.set_complete_instantly(instantly);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,17 @@ use std::cmp::{max, min};
|
||||
use std::rc::Rc;
|
||||
|
||||
use niri::layout::{
|
||||
InteractiveResizeData, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot,
|
||||
ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement,
|
||||
LayoutElementRenderSnapshot,
|
||||
};
|
||||
use niri::render_helpers::offscreen::OffscreenData;
|
||||
use niri::render_helpers::renderer::NiriRenderer;
|
||||
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use niri::render_helpers::{RenderTarget, SplitElements};
|
||||
use niri::utils::transaction::Transaction;
|
||||
use niri::window::ResolvedWindowRules;
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use smithay::backend::renderer::element::{Id, Kind};
|
||||
use smithay::output::Output;
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::output::{self, Output};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform};
|
||||
|
||||
@@ -37,7 +40,7 @@ impl TestWindow {
|
||||
let size = Size::from((100, 200));
|
||||
let min_size = Size::from((0, 0));
|
||||
let max_size = Size::from((0, 0));
|
||||
let buffer = SolidColorBuffer::new(size, [0.15, 0.64, 0.41, 1.]);
|
||||
let buffer = SolidColorBuffer::new(size.to_f64(), [0.15, 0.64, 0.41, 1.]);
|
||||
|
||||
Self {
|
||||
id,
|
||||
@@ -49,7 +52,7 @@ impl TestWindow {
|
||||
buffer,
|
||||
pending_fullscreen: false,
|
||||
csd_shadow_width: 0,
|
||||
csd_shadow_buffer: SolidColorBuffer::new((0, 0), [0., 0., 0., 0.3]),
|
||||
csd_shadow_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 0.3]),
|
||||
})),
|
||||
}
|
||||
}
|
||||
@@ -85,7 +88,7 @@ impl TestWindow {
|
||||
|
||||
let mut new_size = inner.size;
|
||||
|
||||
if let Some(size) = inner.requested_size.take() {
|
||||
if let Some(size) = inner.requested_size {
|
||||
assert!(size.w >= 0);
|
||||
assert!(size.h >= 0);
|
||||
|
||||
@@ -112,14 +115,14 @@ impl TestWindow {
|
||||
|
||||
if inner.size != new_size {
|
||||
inner.size = new_size;
|
||||
inner.buffer.resize(new_size);
|
||||
inner.buffer.resize(new_size.to_f64());
|
||||
rv = true;
|
||||
}
|
||||
|
||||
let mut csd_shadow_size = new_size;
|
||||
csd_shadow_size.w += inner.csd_shadow_width * 2;
|
||||
csd_shadow_size.h += inner.csd_shadow_width * 2;
|
||||
inner.csd_shadow_buffer.resize(csd_shadow_size);
|
||||
inner.csd_shadow_buffer.resize(csd_shadow_size.to_f64());
|
||||
|
||||
rv
|
||||
}
|
||||
@@ -147,8 +150,8 @@ impl LayoutElement for TestWindow {
|
||||
fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
_renderer: &mut R,
|
||||
location: Point<i32, Logical>,
|
||||
scale: Scale<f64>,
|
||||
location: Point<f64, Logical>,
|
||||
_scale: Scale<f64>,
|
||||
alpha: f32,
|
||||
_target: RenderTarget,
|
||||
) -> SplitElements<LayoutElementRenderElement<R>> {
|
||||
@@ -158,17 +161,15 @@ impl LayoutElement for TestWindow {
|
||||
normal: vec![
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.buffer,
|
||||
location.to_physical_precise_round(scale),
|
||||
scale,
|
||||
location,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.csd_shadow_buffer,
|
||||
(location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width)))
|
||||
.to_physical_precise_round(scale),
|
||||
scale,
|
||||
location
|
||||
- Point::from((inner.csd_shadow_width, inner.csd_shadow_width)).to_f64(),
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
@@ -178,13 +179,15 @@ impl LayoutElement for TestWindow {
|
||||
}
|
||||
}
|
||||
|
||||
fn request_size(&mut self, size: Size<i32, Logical>, _animate: bool) {
|
||||
fn request_size(
|
||||
&mut self,
|
||||
size: Size<i32, Logical>,
|
||||
is_fullscreen: bool,
|
||||
_animate: bool,
|
||||
_transaction: Option<Transaction>,
|
||||
) {
|
||||
self.inner.borrow_mut().requested_size = Some(size);
|
||||
self.inner.borrow_mut().pending_fullscreen = false;
|
||||
}
|
||||
|
||||
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
|
||||
self.inner.borrow_mut().pending_fullscreen = true;
|
||||
self.inner.borrow_mut().pending_fullscreen = is_fullscreen;
|
||||
}
|
||||
|
||||
fn min_size(&self) -> Size<i32, Logical> {
|
||||
@@ -199,7 +202,7 @@ impl LayoutElement for TestWindow {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_preferred_scale_transform(&self, _scale: i32, _transform: Transform) {}
|
||||
fn set_preferred_scale_transform(&self, _scale: output::Scale, _transform: Transform) {}
|
||||
|
||||
fn has_ssd(&self) -> bool {
|
||||
false
|
||||
@@ -209,14 +212,24 @@ impl LayoutElement for TestWindow {
|
||||
|
||||
fn output_leave(&self, _output: &Output) {}
|
||||
|
||||
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
|
||||
fn set_offscreen_data(&self, _data: Option<OffscreenData>) {}
|
||||
|
||||
fn set_activated(&mut self, _active: bool) {}
|
||||
|
||||
fn set_active_in_column(&mut self, _active: bool) {}
|
||||
|
||||
fn set_floating(&mut self, _floating: bool) {}
|
||||
|
||||
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
|
||||
|
||||
fn is_ignoring_opacity_window_rule(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn configure_intent(&self) -> ConfigureIntent {
|
||||
ConfigureIntent::CanSend
|
||||
}
|
||||
|
||||
fn send_pending_configure(&mut self) {}
|
||||
|
||||
fn is_fullscreen(&self) -> bool {
|
||||
@@ -227,6 +240,14 @@ impl LayoutElement for TestWindow {
|
||||
self.inner.borrow().pending_fullscreen
|
||||
}
|
||||
|
||||
fn requested_size(&self) -> Option<Size<i32, Logical>> {
|
||||
self.inner.borrow().requested_size
|
||||
}
|
||||
|
||||
fn is_child_of(&self, _parent: &Self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn refresh(&self) {}
|
||||
|
||||
fn rules(&self) -> &ResolvedWindowRules {
|
||||
@@ -246,9 +267,13 @@ impl LayoutElement for TestWindow {
|
||||
|
||||
fn cancel_interactive_resize(&mut self) {}
|
||||
|
||||
fn update_interactive_resize(&mut self, _serial: Serial) {}
|
||||
fn on_commit(&mut self, _serial: Serial) {}
|
||||
|
||||
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_urgent(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
%bcond_without check
|
||||
|
||||
%global cargo_install_lib 0
|
||||
|
||||
# We want panic backtraces to work without installing the debuginfo package,
|
||||
# so we leave the debuginfo in the main binary.
|
||||
%global debug_package %{nil}
|
||||
%global __strip /bin/true
|
||||
|
||||
# To reduce the file size, do some convincing of rust-srpm-macros
|
||||
# to leave alone the chosen debug settings from Cargo.toml.
|
||||
%global rustflags_debuginfo please-remove-me
|
||||
%global build_rustflags %{shrink:
|
||||
-Copt-level=%rustflags_opt_level
|
||||
-Ccodegen-units=%rustflags_codegen_units
|
||||
-Cstrip=none
|
||||
%{expr:0%{?_include_frame_pointers} && ("%{_arch}" != "ppc64le" && "%{_arch}" != "s390x" && "%{_arch}" != "i386") ? "-Cforce-frame-pointers=yes" : ""}
|
||||
-Clink-arg=-Wl,-z,relro
|
||||
-Clink-arg=-Wl,-z,now
|
||||
%[0%{?_package_note_status} ? "-Clink-arg=%_package_note_flags" : ""]
|
||||
--cap-lints=warn
|
||||
}
|
||||
|
||||
# Convince rust-srpm-macros to use Cargo.lock with the Smithay commit.
|
||||
%global __cargo_common_opts %{?_smp_mflags} -Z avoid-dev-deps --locked
|
||||
|
||||
%global version {{{ git_dir_version }}}
|
||||
|
||||
Name: niri
|
||||
Version: %{version}
|
||||
Release: 1%{?dist}
|
||||
Summary: Scrollable-tiling Wayland compositor
|
||||
|
||||
SourceLicense: GPL-3.0-or-later
|
||||
|
||||
# 0BSD OR MIT OR Apache-2.0
|
||||
# Apache-2.0
|
||||
# Apache-2.0 OR BSL-1.0
|
||||
# Apache-2.0 OR MIT
|
||||
# Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT
|
||||
# BSD-2-Clause
|
||||
# BSD-2-Clause OR Apache-2.0 OR MIT
|
||||
# BSD-3-Clause OR MIT OR Apache-2.0
|
||||
# GPL-3.0-or-later
|
||||
# ISC
|
||||
# MIT
|
||||
# MIT AND (MIT OR Apache-2.0)
|
||||
# MIT OR Apache-2.0
|
||||
# (MIT OR Apache-2.0) AND BSD-3-Clause
|
||||
# (MIT OR Apache-2.0) AND Unicode-3.0
|
||||
# MIT OR Apache-2.0 OR Zlib
|
||||
# MIT OR Zlib OR Apache-2.0
|
||||
# MPL-2.0
|
||||
# Unicode-3.0
|
||||
# Unlicense OR MIT
|
||||
# Zlib OR Apache-2.0 OR MIT
|
||||
License: (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT AND (MIT OR Apache-2.0)) AND (MIT OR Apache-2.0) AND ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unicode-3.0) AND (Unlicense OR MIT) AND (Zlib OR Apache-2.0 OR MIT)
|
||||
# LICENSE.dependencies contains a full license breakdown
|
||||
|
||||
URL: https://github.com/YaLTeR/niri
|
||||
VCS: {{{ git_dir_vcs }}}
|
||||
Source: {{{ git_dir_pack }}}
|
||||
|
||||
BuildRequires: cargo-rpm-macros >= 26
|
||||
BuildRequires: pkgconfig(udev)
|
||||
BuildRequires: pkgconfig(gbm)
|
||||
BuildRequires: pkgconfig(xkbcommon)
|
||||
BuildRequires: wayland-devel
|
||||
BuildRequires: pkgconfig(libinput)
|
||||
BuildRequires: pkgconfig(dbus-1)
|
||||
BuildRequires: pkgconfig(systemd)
|
||||
BuildRequires: pkgconfig(libseat)
|
||||
BuildRequires: pkgconfig(libdisplay-info)
|
||||
BuildRequires: pipewire-devel
|
||||
BuildRequires: pango-devel
|
||||
BuildRequires: cairo-gobject-devel
|
||||
# Needed for pipewire-rs
|
||||
BuildRequires: clang
|
||||
|
||||
Requires: mesa-dri-drivers
|
||||
Requires: mesa-libEGL
|
||||
|
||||
# Portal implementations used by niri
|
||||
Recommends: xdg-desktop-portal-gtk
|
||||
Recommends: xdg-desktop-portal-gnome
|
||||
Recommends: gnome-keyring
|
||||
|
||||
# Suggested utilities, bound in the default config
|
||||
Recommends: alacritty
|
||||
Recommends: fuzzel
|
||||
Recommends: swaylock
|
||||
Recommends: waybar
|
||||
# Suggested utilities
|
||||
Recommends: swaybg
|
||||
Recommends: mako
|
||||
Recommends: swayidle
|
||||
|
||||
%description
|
||||
A scrollable-tiling Wayland compositor.
|
||||
|
||||
Windows are arranged in columns on an infinite strip going to the right.
|
||||
Opening a new window never causes existing windows to resize.
|
||||
|
||||
%prep
|
||||
{{{ git_dir_setup_macro }}}
|
||||
|
||||
%cargo_prep -N
|
||||
|
||||
# We're doing an online build.
|
||||
sed -i 's/^offline = true$//' .cargo/config.toml
|
||||
|
||||
# Final step in leaving alone our debug settings.
|
||||
sed -i 's/.*please-remove-me$//' .cargo/config.toml
|
||||
|
||||
# Set the commit string.
|
||||
sed -i 's/\[env\]/[env]\nNIRI_BUILD_COMMIT="%{version}"/' .cargo/config.toml
|
||||
|
||||
%build
|
||||
%cargo_build
|
||||
|
||||
%install
|
||||
%cargo_install
|
||||
|
||||
install -Dm755 -t %{buildroot}%{_bindir} ./resources/niri-session
|
||||
install -Dm644 -t %{buildroot}%{_datadir}/wayland-sessions ./resources/niri.desktop
|
||||
install -Dm644 -t %{buildroot}%{_datadir}/xdg-desktop-portal ./resources/niri-portals.conf
|
||||
install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri.service
|
||||
install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri-shutdown.target
|
||||
|
||||
%if %{with check}
|
||||
%check
|
||||
%cargo_test -- --workspace --exclude niri-visual-tests
|
||||
%endif
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%doc README.md
|
||||
%doc resources/default-config.kdl
|
||||
%doc wiki
|
||||
%{_bindir}/niri
|
||||
%{_bindir}/niri-session
|
||||
%{_datadir}/wayland-sessions/niri.desktop
|
||||
%dir %{_datadir}/xdg-desktop-portal
|
||||
%{_datadir}/xdg-desktop-portal/niri-portals.conf
|
||||
%{_userunitdir}/niri.service
|
||||
%{_userunitdir}/niri-shutdown.target
|
||||
|
||||
%changelog
|
||||
{{{ git_dir_changelog }}}
|
||||
|
||||
+140
-15
@@ -1,7 +1,7 @@
|
||||
// This config is in the KDL format: https://kdl.dev
|
||||
// "/-" comments out the following node.
|
||||
// Check the wiki for a full description of the configuration:
|
||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Overview
|
||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Introduction
|
||||
|
||||
// Input device configuration.
|
||||
// Find the full list of options on the wiki:
|
||||
@@ -16,30 +16,51 @@ input {
|
||||
// layout "us,ru"
|
||||
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
|
||||
}
|
||||
|
||||
// Enable numlock on startup, omitting this setting disables it.
|
||||
numlock
|
||||
}
|
||||
|
||||
// Next sections include libinput settings.
|
||||
// Omitting settings disables them, or leaves them at their default values.
|
||||
touchpad {
|
||||
// off
|
||||
tap
|
||||
// dwt
|
||||
// dwtp
|
||||
// drag false
|
||||
// drag-lock
|
||||
natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "two-finger"
|
||||
// disabled-on-external-mouse
|
||||
}
|
||||
|
||||
mouse {
|
||||
// off
|
||||
// natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "no-scroll"
|
||||
}
|
||||
|
||||
trackpoint {
|
||||
// off
|
||||
// natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "on-button-down"
|
||||
// scroll-button 273
|
||||
// middle-emulation
|
||||
}
|
||||
|
||||
// Uncomment this to make the mouse warp to the center of newly focused windows.
|
||||
// warp-mouse-to-focus
|
||||
|
||||
// Focus windows and outputs automatically when moving the mouse into them.
|
||||
// focus-follows-mouse
|
||||
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
|
||||
// focus-follows-mouse max-scroll-amount="0%"
|
||||
}
|
||||
|
||||
// You can configure outputs by their name, which you can find
|
||||
@@ -60,8 +81,8 @@ input {
|
||||
// Run `niri msg outputs` while inside a niri instance to list all outputs and their modes.
|
||||
mode "1920x1080@120.030"
|
||||
|
||||
// Scale is a floating-point number, but at the moment only integer values work.
|
||||
scale 2.0
|
||||
// You can use integer or fractional scale, for example use 1.5 for 150% scale.
|
||||
scale 2
|
||||
|
||||
// Transform allows to rotate the output counter-clockwise, valid values are:
|
||||
// normal, 90, 180, 270, flipped, flipped-90, flipped-180 and flipped-270.
|
||||
@@ -107,6 +128,9 @@ layout {
|
||||
// fixed 1920
|
||||
}
|
||||
|
||||
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
|
||||
// preset-window-heights { }
|
||||
|
||||
// You can change the default width of the new windows.
|
||||
default-column-width { proportion 0.5; }
|
||||
// If you leave the brackets empty, the windows themselves will decide their initial width.
|
||||
@@ -147,6 +171,7 @@ layout {
|
||||
// The angle is the same as in linear-gradient, and is optional,
|
||||
// defaulting to 180 (top-to-bottom gradient).
|
||||
// You can use any CSS linear-gradient tool on the web to set these up.
|
||||
// Changing the color space is also supported, check the wiki for more info.
|
||||
//
|
||||
// active-gradient from="#80c8ff" to="#bbddff" angle=45
|
||||
|
||||
@@ -167,10 +192,50 @@ layout {
|
||||
active-color "#ffc87f"
|
||||
inactive-color "#505050"
|
||||
|
||||
// Color of the border around windows that request your attention.
|
||||
urgent-color "#9b0000"
|
||||
|
||||
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
|
||||
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||
}
|
||||
|
||||
// You can enable drop shadows for windows.
|
||||
shadow {
|
||||
// Uncomment the next line to enable shadows.
|
||||
// on
|
||||
|
||||
// By default, the shadow draws only around its window, and not behind it.
|
||||
// Uncomment this setting to make the shadow draw behind its window.
|
||||
//
|
||||
// Note that niri has no way of knowing about the CSD window corner
|
||||
// radius. It has to assume that windows have square corners, leading to
|
||||
// shadow artifacts inside the CSD rounded corners. This setting fixes
|
||||
// those artifacts.
|
||||
//
|
||||
// However, instead you may want to set prefer-no-csd and/or
|
||||
// geometry-corner-radius. Then, niri will know the corner radius and
|
||||
// draw the shadow correctly, without having to draw it behind the
|
||||
// window. These will also remove client-side shadows if the window
|
||||
// draws any.
|
||||
//
|
||||
// draw-behind-window true
|
||||
|
||||
// You can change how shadows look. The values below are in logical
|
||||
// pixels and match the CSS box-shadow properties.
|
||||
|
||||
// Softness controls the shadow blur radius.
|
||||
softness 30
|
||||
|
||||
// Spread expands the shadow.
|
||||
spread 5
|
||||
|
||||
// Offset moves the shadow relative to the window.
|
||||
offset x=0 y=5
|
||||
|
||||
// You can also change the shadow color and opacity.
|
||||
color "#0007"
|
||||
}
|
||||
|
||||
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
|
||||
// You can think of them as a kind of outer gaps. They are set in logical pixels.
|
||||
// Left and right struts will cause the next window to the side to always be visible.
|
||||
@@ -187,11 +252,16 @@ layout {
|
||||
// Add lines like this to spawn processes at startup.
|
||||
// Note that running niri as a session supports xdg-desktop-autostart,
|
||||
// which may be more convenient to use.
|
||||
// spawn-at-startup "alacritty" "-e" "fish"
|
||||
// See the binds section below for more spawn examples.
|
||||
|
||||
// This line starts waybar, a commonly used bar for Wayland compositors.
|
||||
spawn-at-startup "waybar"
|
||||
|
||||
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
|
||||
// If the client will specifically ask for CSD, the request will be honored.
|
||||
// Additionally, clients will be informed that they are tiled, removing some rounded corners.
|
||||
// Additionally, clients will be informed that they are tiled, removing some client-side rounded corners.
|
||||
// This option will also fix border/focus ring drawing behind some semitransparent windows.
|
||||
// After enabling or disabling this, you need to restart the apps for this to take effect.
|
||||
// prefer-no-csd
|
||||
|
||||
// You can change the path where screenshots are saved.
|
||||
@@ -227,6 +297,15 @@ window-rule {
|
||||
default-column-width {}
|
||||
}
|
||||
|
||||
// Open the Firefox picture-in-picture player as floating by default.
|
||||
window-rule {
|
||||
// This app-id regular expression will work for both:
|
||||
// - host Firefox (app-id is "firefox")
|
||||
// - Flatpak Firefox (app-id is "org.mozilla.firefox")
|
||||
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
|
||||
open-floating true
|
||||
}
|
||||
|
||||
// Example: block out two password managers from screen capture.
|
||||
// (This example rule is commented out with a "/-" in front.)
|
||||
/-window-rule {
|
||||
@@ -239,6 +318,13 @@ window-rule {
|
||||
// block-out-from "screencast"
|
||||
}
|
||||
|
||||
// Example: enable rounded corners for all windows.
|
||||
// (This example rule is commented out with a "/-" in front.)
|
||||
/-window-rule {
|
||||
geometry-corner-radius 12
|
||||
clip-to-geometry true
|
||||
}
|
||||
|
||||
binds {
|
||||
// Keys consist of modifiers separated by + signs, followed by an XKB key name
|
||||
// in the end. To find an XKB name for a particular key, you may use a program
|
||||
@@ -255,11 +341,12 @@ binds {
|
||||
Mod+Shift+Slash { show-hotkey-overlay; }
|
||||
|
||||
// Suggested binds for running programs: terminal, app launcher, screen locker.
|
||||
Mod+T { spawn "alacritty"; }
|
||||
Mod+D { spawn "fuzzel"; }
|
||||
Super+Alt+L { spawn "swaylock"; }
|
||||
Mod+T hotkey-overlay-title="Open a Terminal: alacritty" { spawn "alacritty"; }
|
||||
Mod+D hotkey-overlay-title="Run an Application: fuzzel" { spawn "fuzzel"; }
|
||||
Super+Alt+L hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; }
|
||||
|
||||
// You can also use a shell:
|
||||
// You can also use a shell. Do this if you need pipes, multiple commands, etc.
|
||||
// Note: the entire command goes as a single argument in the end.
|
||||
// Mod+T { spawn "bash" "-c" "notify-send hello && exec alacritty"; }
|
||||
|
||||
// Example volume keys mappings for PipeWire & WirePlumber.
|
||||
@@ -269,6 +356,11 @@ binds {
|
||||
XF86AudioMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
|
||||
XF86AudioMicMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }
|
||||
|
||||
// Open/close the Overview: a zoomed-out view of workspaces and windows.
|
||||
// You can also move the mouse into the top-left hot corner,
|
||||
// or do a four-finger swipe up on a touchpad.
|
||||
Mod+O repeat=false { toggle-overview; }
|
||||
|
||||
Mod+Q { close-window; }
|
||||
|
||||
Mod+Left { focus-column-left; }
|
||||
@@ -410,19 +502,32 @@ binds {
|
||||
// Switches focus between the current and the previous workspace.
|
||||
// Mod+Tab { focus-workspace-previous; }
|
||||
|
||||
// The following binds move the focused window in and out of a column.
|
||||
// If the window is alone, they will consume it into the nearby column to the side.
|
||||
// If the window is already in a column, they will expel it out.
|
||||
Mod+BracketLeft { consume-or-expel-window-left; }
|
||||
Mod+BracketRight { consume-or-expel-window-right; }
|
||||
|
||||
// Consume one window from the right to the bottom of the focused column.
|
||||
Mod+Comma { consume-window-into-column; }
|
||||
// Expel the bottom window from the focused column to the right.
|
||||
Mod+Period { expel-window-from-column; }
|
||||
|
||||
// There are also commands that consume or expel a single window to the side.
|
||||
// Mod+BracketLeft { consume-or-expel-window-left; }
|
||||
// Mod+BracketRight { consume-or-expel-window-right; }
|
||||
|
||||
Mod+R { switch-preset-column-width; }
|
||||
Mod+Shift+R { reset-window-height; }
|
||||
Mod+Shift+R { switch-preset-window-height; }
|
||||
Mod+Ctrl+R { reset-window-height; }
|
||||
Mod+F { maximize-column; }
|
||||
Mod+Shift+F { fullscreen-window; }
|
||||
|
||||
// Expand the focused column to space not taken up by other fully visible columns.
|
||||
// Makes the column "fill the rest of the space".
|
||||
Mod+Ctrl+F { expand-column-to-available-width; }
|
||||
|
||||
Mod+C { center-column; }
|
||||
|
||||
// Center all fully visible columns on screen.
|
||||
Mod+Ctrl+C { center-visible-columns; }
|
||||
|
||||
// Finer width adjustments.
|
||||
// This command can also:
|
||||
// * set width in pixels: "1000"
|
||||
@@ -438,6 +543,15 @@ binds {
|
||||
Mod+Shift+Minus { set-window-height "-10%"; }
|
||||
Mod+Shift+Equal { set-window-height "+10%"; }
|
||||
|
||||
// Move the focused window between the floating and the tiling layout.
|
||||
Mod+V { toggle-window-floating; }
|
||||
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
|
||||
|
||||
// Toggle tabbed column display mode.
|
||||
// Windows in this column will appear as vertical tabs,
|
||||
// rather than stacked on top of each other.
|
||||
Mod+W { toggle-column-tabbed-display; }
|
||||
|
||||
// Actions to switch layouts.
|
||||
// Note: if you uncomment these, make sure you do NOT have
|
||||
// a matching layout switch hotkey configured in xkb options above.
|
||||
@@ -450,8 +564,19 @@ binds {
|
||||
Ctrl+Print { screenshot-screen; }
|
||||
Alt+Print { screenshot-window; }
|
||||
|
||||
// Applications such as remote-desktop clients and software KVM switches may
|
||||
// request that niri stops processing the keyboard shortcuts defined here
|
||||
// so they may, for example, forward the key presses as-is to a remote machine.
|
||||
// It's a good idea to bind an escape hatch to toggle the inhibitor,
|
||||
// so a buggy application can't hold your session hostage.
|
||||
//
|
||||
// The allow-inhibiting=false property can be applied to other binds as well,
|
||||
// which ensures niri always processes them, even when an inhibitor is active.
|
||||
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
|
||||
|
||||
// The quit action will show a confirmation dialog to avoid accidental exits.
|
||||
Mod+Shift+E { quit; }
|
||||
Ctrl+Alt+Delete { quit; }
|
||||
|
||||
// Powers off the monitors. To turn them back on, do any input like
|
||||
// moving the mouse or pressing any other key.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
type = process
|
||||
command = niri --session
|
||||
restart = false
|
||||
working-dir = $HOME
|
||||
depends-on = dbus
|
||||
after = niri-shutdown
|
||||
chain-to = niri-shutdown
|
||||
options: always-chain
|
||||
@@ -0,0 +1,3 @@
|
||||
type = scripted
|
||||
command = dinitctl -u setenv WAYLAND_DISPLAY= XDG_SESSION_TYPE= XDG_CURRENT_DESKTOP= NIRI_SOCKET=
|
||||
restart = false
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="mutter_x11_interop">
|
||||
<description summary="X11 interoperability helper">
|
||||
This protocol is intended to be used by the portal backend to map Wayland
|
||||
dialogs as modal dialogs on top of X11 windows.
|
||||
</description>
|
||||
|
||||
<interface name="mutter_x11_interop" version="1">
|
||||
<description summary="X11 interoperability helper"/>
|
||||
|
||||
<request name="destroy" type="destructor"/>
|
||||
|
||||
<request name="set_x11_parent">
|
||||
<arg name="surface" type="object" interface="wl_surface"/>
|
||||
<arg name="xwindow" type="uint"/>
|
||||
</request>
|
||||
</interface>
|
||||
</protocol>
|
||||
@@ -1,3 +1,5 @@
|
||||
[preferred]
|
||||
default=gnome;gtk;
|
||||
org.freedesktop.impl.portal.Access=gtk;
|
||||
org.freedesktop.impl.portal.Notification=gtk;
|
||||
org.freedesktop.impl.portal.Secret=gnome-keyring;
|
||||
|
||||
+47
-27
@@ -11,31 +11,51 @@ if [ -n "$SHELL" ] &&
|
||||
fi
|
||||
fi
|
||||
|
||||
# Make sure there's no already running session.
|
||||
if systemctl --user -q is-active niri.service; then
|
||||
echo 'A niri session is already running.'
|
||||
exit 1
|
||||
# Try to detect the service manager that is being used
|
||||
if hash systemctl >/dev/null 2>&1; then
|
||||
# Make sure there's no already running session.
|
||||
if systemctl --user -q is-active niri.service; then
|
||||
echo 'A niri session is already running.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Reset failed state of all user units.
|
||||
systemctl --user reset-failed
|
||||
|
||||
# Import the login manager environment.
|
||||
systemctl --user import-environment
|
||||
|
||||
# DBus activation environment is independent from systemd. While most of
|
||||
# dbus-activated services are already using `SystemdService` directive, some
|
||||
# still don't and thus we should set the dbus environment with a separate
|
||||
# command.
|
||||
if hash dbus-update-activation-environment 2>/dev/null; then
|
||||
dbus-update-activation-environment --all
|
||||
fi
|
||||
|
||||
# Start niri and wait for it to terminate.
|
||||
systemctl --user --wait start niri.service
|
||||
|
||||
# Force stop of graphical-session.target.
|
||||
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
|
||||
|
||||
# Unset environment that we've set.
|
||||
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
|
||||
elif hash dinitctl >/dev/null 2>&1; then
|
||||
# Check that the user dinit daemon is running
|
||||
if ! pgrep -u "$(id -u)" dinit >/dev/null 2>&1; then
|
||||
echo "dinit user daemon is not running."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make sure there's no already running session.
|
||||
if dinitctl --user is-started niri >/dev/null 2>&1; then
|
||||
echo 'A niri session is already running.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start niri
|
||||
dinitctl --user start niri
|
||||
else
|
||||
echo "No systemd or dinit detected, please use niri --session instead."
|
||||
fi
|
||||
|
||||
# Reset failed state of all user units.
|
||||
systemctl --user reset-failed
|
||||
|
||||
# Import the login manager environment.
|
||||
systemctl --user import-environment
|
||||
|
||||
# DBus activation environment is independent from systemd. While most of
|
||||
# dbus-activated services are already using `SystemdService` directive, some
|
||||
# still don't and thus we should set the dbus environment with a separate
|
||||
# command.
|
||||
if hash dbus-update-activation-environment 2>/dev/null; then
|
||||
dbus-update-activation-environment --all
|
||||
fi
|
||||
|
||||
# Start niri and wait for it to terminate.
|
||||
systemctl --user --wait start niri.service
|
||||
|
||||
# Force stop of grahical-session.target.
|
||||
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
|
||||
|
||||
# Unset environment that we've set.
|
||||
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv=refresh content=0;url=niri_ipc/index.html />
|
||||
</head>
|
||||
</html>
|
||||
@@ -0,0 +1,202 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
/// Shareable lazy clock that can change rate.
|
||||
///
|
||||
/// The clock will fetch the time once and then retain it until explicitly cleared with
|
||||
/// [`Clock::clear`].
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Clock {
|
||||
inner: Rc<RefCell<AdjustableClock>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct LazyClock {
|
||||
time: Option<Duration>,
|
||||
}
|
||||
|
||||
/// Clock that can adjust its rate.
|
||||
#[derive(Debug)]
|
||||
struct AdjustableClock {
|
||||
inner: LazyClock,
|
||||
current_time: Duration,
|
||||
last_seen_time: Duration,
|
||||
rate: f64,
|
||||
complete_instantly: bool,
|
||||
}
|
||||
|
||||
impl Clock {
|
||||
/// Creates a new clock with the given time.
|
||||
pub fn with_time(time: Duration) -> Self {
|
||||
let clock = AdjustableClock::new(LazyClock::with_time(time));
|
||||
Self {
|
||||
inner: Rc::new(RefCell::new(clock)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current time.
|
||||
pub fn now(&self) -> Duration {
|
||||
self.inner.borrow_mut().now()
|
||||
}
|
||||
|
||||
/// Returns the underlying time not adjusted for rate change.
|
||||
pub fn now_unadjusted(&self) -> Duration {
|
||||
self.inner.borrow_mut().inner.now()
|
||||
}
|
||||
|
||||
/// Sets the unadjusted clock time.
|
||||
pub fn set_unadjusted(&mut self, time: Duration) {
|
||||
self.inner.borrow_mut().inner.set(time);
|
||||
}
|
||||
|
||||
/// Clears the stored time so it's re-fetched again next.
|
||||
pub fn clear(&mut self) {
|
||||
self.inner.borrow_mut().inner.clear();
|
||||
}
|
||||
|
||||
/// Gets the clock rate.
|
||||
pub fn rate(&self) -> f64 {
|
||||
self.inner.borrow().rate()
|
||||
}
|
||||
|
||||
/// Sets the clock rate.
|
||||
pub fn set_rate(&mut self, rate: f64) {
|
||||
self.inner.borrow_mut().set_rate(rate);
|
||||
}
|
||||
|
||||
/// Returns whether animations should complete instantly.
|
||||
pub fn should_complete_instantly(&self) -> bool {
|
||||
self.inner.borrow().should_complete_instantly()
|
||||
}
|
||||
|
||||
/// Sets whether animations should complete instantly.
|
||||
pub fn set_complete_instantly(&mut self, value: bool) {
|
||||
self.inner.borrow_mut().set_complete_instantly(value);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Clock {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Rc::ptr_eq(&self.inner, &other.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Clock {}
|
||||
|
||||
impl LazyClock {
|
||||
pub fn with_time(time: Duration) -> Self {
|
||||
Self { time: Some(time) }
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.time = None;
|
||||
}
|
||||
|
||||
pub fn set(&mut self, time: Duration) {
|
||||
self.time = Some(time);
|
||||
}
|
||||
|
||||
pub fn now(&mut self) -> Duration {
|
||||
*self.time.get_or_insert_with(get_monotonic_time)
|
||||
}
|
||||
}
|
||||
|
||||
impl AdjustableClock {
|
||||
pub fn new(mut inner: LazyClock) -> Self {
|
||||
let time = inner.now();
|
||||
Self {
|
||||
inner,
|
||||
current_time: time,
|
||||
last_seen_time: time,
|
||||
rate: 1.,
|
||||
complete_instantly: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rate(&self) -> f64 {
|
||||
self.rate
|
||||
}
|
||||
|
||||
pub fn set_rate(&mut self, rate: f64) {
|
||||
self.rate = rate.clamp(0., 1000.);
|
||||
}
|
||||
|
||||
pub fn should_complete_instantly(&self) -> bool {
|
||||
self.complete_instantly
|
||||
}
|
||||
|
||||
pub fn set_complete_instantly(&mut self, value: bool) {
|
||||
self.complete_instantly = value;
|
||||
}
|
||||
|
||||
pub fn now(&mut self) -> Duration {
|
||||
let time = self.inner.now();
|
||||
|
||||
if self.last_seen_time == time {
|
||||
return self.current_time;
|
||||
}
|
||||
|
||||
if self.last_seen_time < time {
|
||||
let delta = time - self.last_seen_time;
|
||||
let delta = delta.mul_f64(self.rate);
|
||||
self.current_time = self.current_time.saturating_add(delta);
|
||||
} else {
|
||||
let delta = self.last_seen_time - time;
|
||||
let delta = delta.mul_f64(self.rate);
|
||||
self.current_time = self.current_time.saturating_sub(delta);
|
||||
}
|
||||
|
||||
self.last_seen_time = time;
|
||||
self.current_time
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AdjustableClock {
|
||||
fn default() -> Self {
|
||||
Self::new(LazyClock::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn frozen_clock() {
|
||||
let mut clock = Clock::with_time(Duration::ZERO);
|
||||
assert_eq!(clock.now(), Duration::ZERO);
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(100));
|
||||
assert_eq!(clock.now(), Duration::from_millis(100));
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(200));
|
||||
assert_eq!(clock.now(), Duration::from_millis(200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rate_change() {
|
||||
let mut clock = Clock::with_time(Duration::ZERO);
|
||||
clock.set_rate(0.5);
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(100));
|
||||
assert_eq!(clock.now_unadjusted(), Duration::from_millis(100));
|
||||
assert_eq!(clock.now(), Duration::from_millis(50));
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(200));
|
||||
assert_eq!(clock.now_unadjusted(), Duration::from_millis(200));
|
||||
assert_eq!(clock.now(), Duration::from_millis(100));
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(150));
|
||||
assert_eq!(clock.now_unadjusted(), Duration::from_millis(150));
|
||||
assert_eq!(clock.now(), Duration::from_millis(75));
|
||||
|
||||
clock.set_rate(2.0);
|
||||
|
||||
clock.set_unadjusted(Duration::from_millis(250));
|
||||
assert_eq!(clock.now_unadjusted(), Duration::from_millis(250));
|
||||
assert_eq!(clock.now(), Duration::from_millis(275));
|
||||
}
|
||||
}
|
||||
+85
-106
@@ -2,16 +2,14 @@ use std::time::Duration;
|
||||
|
||||
use keyframe::functions::{EaseOutCubic, EaseOutQuad};
|
||||
use keyframe::EasingFunction;
|
||||
use portable_atomic::{AtomicF64, Ordering};
|
||||
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
mod spring;
|
||||
pub use spring::{Spring, SpringParams};
|
||||
|
||||
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
|
||||
mod clock;
|
||||
pub use clock::Clock;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Animation {
|
||||
from: f64,
|
||||
to: f64,
|
||||
@@ -23,7 +21,7 @@ pub struct Animation {
|
||||
/// Best effort; not always exactly precise.
|
||||
clamped_duration: Duration,
|
||||
start_time: Duration,
|
||||
current_time: Duration,
|
||||
clock: Clock,
|
||||
kind: Kind,
|
||||
}
|
||||
|
||||
@@ -48,11 +46,17 @@ pub enum Curve {
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub fn new(from: f64, to: f64, initial_velocity: f64, config: niri_config::Animation) -> Self {
|
||||
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
|
||||
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
||||
pub fn new(
|
||||
clock: Clock,
|
||||
from: f64,
|
||||
to: f64,
|
||||
initial_velocity: f64,
|
||||
config: niri_config::Animation,
|
||||
) -> Self {
|
||||
// Scale the velocity by rate to keep the touchpad gestures feeling right.
|
||||
let initial_velocity = initial_velocity / clock.rate().max(0.001);
|
||||
|
||||
let mut rv = Self::ease(from, to, initial_velocity, 0, Curve::EaseOutCubic);
|
||||
let mut rv = Self::ease(clock, from, to, initial_velocity, 0, Curve::EaseOutCubic);
|
||||
if config.off {
|
||||
rv.is_off = true;
|
||||
return rv;
|
||||
@@ -71,7 +75,6 @@ impl Animation {
|
||||
}
|
||||
|
||||
let start_time = self.start_time;
|
||||
let current_time = self.current_time;
|
||||
|
||||
match config.kind {
|
||||
niri_config::AnimationKind::Spring(p) => {
|
||||
@@ -83,10 +86,11 @@ impl Animation {
|
||||
initial_velocity: self.initial_velocity,
|
||||
params,
|
||||
};
|
||||
*self = Self::spring(spring);
|
||||
*self = Self::spring(self.clock.clone(), spring);
|
||||
}
|
||||
niri_config::AnimationKind::Easing(p) => {
|
||||
*self = Self::ease(
|
||||
self.clock.clone(),
|
||||
self.from,
|
||||
self.to,
|
||||
self.initial_velocity,
|
||||
@@ -97,20 +101,20 @@ impl Animation {
|
||||
}
|
||||
|
||||
self.start_time = start_time;
|
||||
self.current_time = current_time;
|
||||
}
|
||||
|
||||
/// Restarts the animation using the previous config.
|
||||
pub fn restarted(self, from: f64, to: f64, initial_velocity: f64) -> Self {
|
||||
pub fn restarted(&self, from: f64, to: f64, initial_velocity: f64) -> Self {
|
||||
if self.is_off {
|
||||
return self;
|
||||
return self.clone();
|
||||
}
|
||||
|
||||
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
|
||||
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
||||
// Scale the velocity by rate to keep the touchpad gestures feeling right.
|
||||
let initial_velocity = initial_velocity / self.clock.rate().max(0.001);
|
||||
|
||||
match self.kind {
|
||||
Kind::Easing { curve } => Self::ease(
|
||||
self.clock.clone(),
|
||||
from,
|
||||
to,
|
||||
initial_velocity,
|
||||
@@ -119,28 +123,37 @@ impl Animation {
|
||||
),
|
||||
Kind::Spring(spring) => {
|
||||
let spring = Spring {
|
||||
from: self.from,
|
||||
to: self.to,
|
||||
from,
|
||||
to,
|
||||
initial_velocity: self.initial_velocity,
|
||||
params: spring.params,
|
||||
};
|
||||
Self::spring(spring)
|
||||
Self::spring(self.clock.clone(), spring)
|
||||
}
|
||||
Kind::Deceleration {
|
||||
initial_velocity,
|
||||
deceleration_rate,
|
||||
} => {
|
||||
let threshold = 0.001; // FIXME
|
||||
Self::decelerate(from, initial_velocity, deceleration_rate, threshold)
|
||||
Self::decelerate(
|
||||
self.clock.clone(),
|
||||
from,
|
||||
initial_velocity,
|
||||
deceleration_rate,
|
||||
threshold,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ease(from: f64, to: f64, initial_velocity: f64, duration_ms: u64, curve: Curve) -> Self {
|
||||
// FIXME: ideally we shouldn't use current time here because animations started within the
|
||||
// same frame cycle should have the same start time to be synchronized.
|
||||
let now = get_monotonic_time();
|
||||
|
||||
pub fn ease(
|
||||
clock: Clock,
|
||||
from: f64,
|
||||
to: f64,
|
||||
initial_velocity: f64,
|
||||
duration_ms: u64,
|
||||
curve: Curve,
|
||||
) -> Self {
|
||||
let duration = Duration::from_millis(duration_ms);
|
||||
let kind = Kind::Easing { curve };
|
||||
|
||||
@@ -152,19 +165,15 @@ impl Animation {
|
||||
duration,
|
||||
// Our current curves never overshoot.
|
||||
clamped_duration: duration,
|
||||
start_time: now,
|
||||
current_time: now,
|
||||
start_time: clock.now(),
|
||||
clock,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spring(spring: Spring) -> Self {
|
||||
pub fn spring(clock: Clock, spring: Spring) -> Self {
|
||||
let _span = tracy_client::span!("Animation::spring");
|
||||
|
||||
// FIXME: ideally we shouldn't use current time here because animations started within the
|
||||
// same frame cycle should have the same start time to be synchronized.
|
||||
let now = get_monotonic_time();
|
||||
|
||||
let duration = spring.duration();
|
||||
let clamped_duration = spring.clamped_duration().unwrap_or(duration);
|
||||
let kind = Kind::Spring(spring);
|
||||
@@ -176,22 +185,19 @@ impl Animation {
|
||||
is_off: false,
|
||||
duration,
|
||||
clamped_duration,
|
||||
start_time: now,
|
||||
current_time: now,
|
||||
start_time: clock.now(),
|
||||
clock,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decelerate(
|
||||
clock: Clock,
|
||||
from: f64,
|
||||
initial_velocity: f64,
|
||||
deceleration_rate: f64,
|
||||
threshold: f64,
|
||||
) -> Self {
|
||||
// FIXME: ideally we shouldn't use current time here because animations started within the
|
||||
// same frame cycle should have the same start time to be synchronized.
|
||||
let now = get_monotonic_time();
|
||||
|
||||
let duration_s = if initial_velocity == 0. {
|
||||
0.
|
||||
} else {
|
||||
@@ -214,85 +220,43 @@ impl Animation {
|
||||
is_off: false,
|
||||
duration,
|
||||
clamped_duration: duration,
|
||||
start_time: now,
|
||||
current_time: now,
|
||||
start_time: clock.now(),
|
||||
clock,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_current_time(&mut self, time: Duration) {
|
||||
if self.duration.is_zero() {
|
||||
self.current_time = time;
|
||||
return;
|
||||
}
|
||||
|
||||
let end_time = self.start_time + self.duration;
|
||||
if end_time <= self.current_time {
|
||||
return;
|
||||
}
|
||||
|
||||
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
||||
if slowdown <= f64::EPSILON {
|
||||
// Zero slowdown will cause the animation to end right away.
|
||||
self.current_time = end_time;
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't change current_time (since the incoming time values are always real-time), so
|
||||
// apply the slowdown by shifting the start time to compensate.
|
||||
if self.current_time <= time {
|
||||
let delta = time - self.current_time;
|
||||
|
||||
let max_delta = end_time - self.current_time;
|
||||
let min_slowdown = delta.as_secs_f64() / max_delta.as_secs_f64();
|
||||
if slowdown <= min_slowdown {
|
||||
// Our slowdown value will cause the animation to end right away.
|
||||
self.current_time = end_time;
|
||||
return;
|
||||
}
|
||||
|
||||
let adjusted_delta = delta.div_f64(slowdown);
|
||||
if adjusted_delta >= delta {
|
||||
self.start_time -= adjusted_delta - delta;
|
||||
} else {
|
||||
self.start_time += delta - adjusted_delta;
|
||||
}
|
||||
} else {
|
||||
let delta = self.current_time - time;
|
||||
|
||||
let min_slowdown = delta.as_secs_f64() / self.current_time.as_secs_f64();
|
||||
if slowdown <= min_slowdown {
|
||||
// Current time was about to jump to before the animation had started; let's just
|
||||
// cancel the animation in this case.
|
||||
self.current_time = end_time;
|
||||
return;
|
||||
}
|
||||
|
||||
let adjusted_delta = delta.div_f64(slowdown);
|
||||
if adjusted_delta >= delta {
|
||||
self.start_time += adjusted_delta - delta;
|
||||
} else {
|
||||
self.start_time -= delta - adjusted_delta;
|
||||
}
|
||||
}
|
||||
|
||||
self.current_time = time;
|
||||
}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.current_time >= self.start_time + self.duration
|
||||
if self.clock.should_complete_instantly() {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.clock.now() >= self.start_time + self.duration
|
||||
}
|
||||
|
||||
pub fn is_clamped_done(&self) -> bool {
|
||||
self.current_time >= self.start_time + self.clamped_duration
|
||||
if self.clock.should_complete_instantly() {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.clock.now() >= self.start_time + self.clamped_duration
|
||||
}
|
||||
|
||||
pub fn value(&self) -> f64 {
|
||||
if self.is_done() {
|
||||
pub fn value_at(&self, at: Duration) -> f64 {
|
||||
if at <= self.start_time {
|
||||
// Return from when at == start_time so that when the animations are off, the behavior
|
||||
// within a single event loop cycle (i.e. no time had passed since the start of an
|
||||
// animation) matches the behavior when the animations are on.
|
||||
return self.from;
|
||||
} else if self.start_time + self.duration <= at {
|
||||
return self.to;
|
||||
}
|
||||
|
||||
let passed = self.current_time - self.start_time;
|
||||
if self.clock.should_complete_instantly() {
|
||||
return self.to;
|
||||
}
|
||||
|
||||
let passed = at.saturating_sub(self.start_time);
|
||||
|
||||
match self.kind {
|
||||
Kind::Easing { curve } => {
|
||||
@@ -325,6 +289,10 @@ impl Animation {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(&self) -> f64 {
|
||||
self.value_at(self.clock.now())
|
||||
}
|
||||
|
||||
/// Returns a value that stops at the target value after first reaching it.
|
||||
///
|
||||
/// Best effort; not always exactly precise.
|
||||
@@ -340,11 +308,22 @@ impl Animation {
|
||||
self.to
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn from(&self) -> f64 {
|
||||
self.from
|
||||
}
|
||||
|
||||
pub fn start_time(&self) -> Duration {
|
||||
self.start_time
|
||||
}
|
||||
|
||||
pub fn end_time(&self) -> Duration {
|
||||
self.start_time + self.duration
|
||||
}
|
||||
|
||||
pub fn duration(&self) -> Duration {
|
||||
self.duration
|
||||
}
|
||||
|
||||
pub fn offset(&mut self, offset: f64) {
|
||||
self.from += offset;
|
||||
self.to += offset;
|
||||
|
||||
@@ -54,6 +54,10 @@ impl Spring {
|
||||
return Duration::MAX;
|
||||
}
|
||||
|
||||
if (self.to - self.from).abs() <= f64::EPSILON {
|
||||
return Duration::ZERO;
|
||||
}
|
||||
|
||||
let omega0 = (self.params.stiffness / self.params.mass).sqrt();
|
||||
|
||||
// As first ansatz for the overdamped solution,
|
||||
@@ -90,6 +94,12 @@ impl Spring {
|
||||
|
||||
x1 = (self.to - y0 + m * x0) / m;
|
||||
y1 = self.oscillate(x1);
|
||||
|
||||
// Overdamped springs have some numerical stability issues...
|
||||
if !y1.is_finite() {
|
||||
return Duration::from_secs_f64(x0);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
@@ -166,3 +176,34 @@ impl Spring {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn overdamped_spring_equal_from_to_nan() {
|
||||
let spring = Spring {
|
||||
from: 0.,
|
||||
to: 0.,
|
||||
initial_velocity: 0.,
|
||||
params: SpringParams::new(1.15, 850., 0.0001),
|
||||
};
|
||||
let _ = spring.duration();
|
||||
let _ = spring.clamped_duration();
|
||||
let _ = spring.value_at(Duration::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overdamped_spring_duration_panic() {
|
||||
let spring = Spring {
|
||||
from: 0.,
|
||||
to: 1.,
|
||||
initial_velocity: 0.,
|
||||
params: SpringParams::new(6., 1200., 0.0001),
|
||||
};
|
||||
let _ = spring.duration();
|
||||
let _ = spring.clamped_duration();
|
||||
let _ = spring.value_at(Duration::ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
//! Headless backend for tests.
|
||||
//!
|
||||
//! This can eventually grow into a more complete backend if needed, but for now it's missing some
|
||||
//! crucial parts like rendering.
|
||||
|
||||
use std::mem;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use niri_config::OutputName;
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::element::RenderElementStates;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||
use smithay::utils::Size;
|
||||
use smithay::wayland::presentation::Refresh;
|
||||
|
||||
use super::{IpcOutputMap, OutputId, RenderResult};
|
||||
use crate::niri::{Niri, RedrawState};
|
||||
use crate::utils::{get_monotonic_time, logical_output};
|
||||
|
||||
pub struct Headless {
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
}
|
||||
|
||||
impl Headless {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ipc_outputs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&mut self, _niri: &mut Niri) {}
|
||||
|
||||
pub fn add_output(&mut self, niri: &mut Niri, n: u8, size: (u16, u16)) {
|
||||
let connector = format!("headless-{n}");
|
||||
let make = "niri".to_string();
|
||||
let model = "headless".to_string();
|
||||
let serial = n.to_string();
|
||||
|
||||
let output = Output::new(
|
||||
connector.clone(),
|
||||
PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: make.clone(),
|
||||
model: model.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
let mode = Mode {
|
||||
size: Size::from((i32::from(size.0), i32::from(size.1))),
|
||||
refresh: 60_000,
|
||||
};
|
||||
output.change_current_state(Some(mode), None, None, None);
|
||||
output.set_preferred(mode);
|
||||
|
||||
output.user_data().insert_if_missing(|| OutputName {
|
||||
connector,
|
||||
make: Some(make),
|
||||
model: Some(model),
|
||||
serial: Some(serial),
|
||||
});
|
||||
|
||||
let physical_properties = output.physical_properties();
|
||||
self.ipc_outputs.lock().unwrap().insert(
|
||||
OutputId::next(),
|
||||
niri_ipc::Output {
|
||||
name: output.name(),
|
||||
make: physical_properties.make,
|
||||
model: physical_properties.model,
|
||||
serial: None,
|
||||
physical_size: None,
|
||||
modes: vec![niri_ipc::Mode {
|
||||
width: size.0,
|
||||
height: size.1,
|
||||
refresh_rate: 60_000,
|
||||
is_preferred: true,
|
||||
}],
|
||||
current_mode: Some(0),
|
||||
vrr_supported: false,
|
||||
vrr_enabled: false,
|
||||
logical: Some(logical_output(&output)),
|
||||
},
|
||||
);
|
||||
|
||||
niri.add_output(output, None, false);
|
||||
}
|
||||
|
||||
pub fn seat_name(&self) -> String {
|
||||
"headless".to_owned()
|
||||
}
|
||||
|
||||
pub fn with_primary_renderer<T>(
|
||||
&mut self,
|
||||
_f: impl FnOnce(&mut GlesRenderer) -> T,
|
||||
) -> Option<T> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn render(&mut self, niri: &mut Niri, output: &Output) -> RenderResult {
|
||||
let states = RenderElementStates::default();
|
||||
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &states);
|
||||
presentation_feedbacks.presented::<_, smithay::utils::Monotonic>(
|
||||
get_monotonic_time(),
|
||||
Refresh::Unknown,
|
||||
0,
|
||||
wp_presentation_feedback::Kind::empty(),
|
||||
);
|
||||
|
||||
let output_state = niri.output_state.get_mut(output).unwrap();
|
||||
match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
|
||||
RedrawState::Idle => unreachable!(),
|
||||
RedrawState::Queued => (),
|
||||
RedrawState::WaitingForVBlank { .. } => unreachable!(),
|
||||
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
|
||||
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
|
||||
}
|
||||
|
||||
output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1);
|
||||
|
||||
// FIXME: request redraw on unfinished animations remain
|
||||
|
||||
RenderResult::Submitted
|
||||
}
|
||||
|
||||
pub fn import_dmabuf(&mut self, _dmabuf: &Dmabuf) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
|
||||
self.ipc_outputs.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Headless {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
+66
-9
@@ -2,13 +2,14 @@ use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::{Config, ModKey};
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
|
||||
use crate::input::CompositorMod;
|
||||
use crate::niri::Niri;
|
||||
use crate::utils::id::IdCounter;
|
||||
|
||||
pub mod tty;
|
||||
pub use tty::Tty;
|
||||
@@ -16,9 +17,14 @@ pub use tty::Tty;
|
||||
pub mod winit;
|
||||
pub use winit::Winit;
|
||||
|
||||
pub mod headless;
|
||||
pub use headless::Headless;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Backend {
|
||||
Tty(Tty),
|
||||
Winit(Winit),
|
||||
Headless(Headless),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
@@ -31,13 +37,29 @@ pub enum RenderResult {
|
||||
Skipped,
|
||||
}
|
||||
|
||||
pub type IpcOutputMap = HashMap<String, niri_ipc::Output>;
|
||||
pub type IpcOutputMap = HashMap<OutputId, niri_ipc::Output>;
|
||||
|
||||
static OUTPUT_ID_COUNTER: IdCounter = IdCounter::new();
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct OutputId(u64);
|
||||
|
||||
impl OutputId {
|
||||
fn next() -> OutputId {
|
||||
OutputId(OUTPUT_ID_COUNTER.next())
|
||||
}
|
||||
|
||||
pub fn get(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
pub fn init(&mut self, niri: &mut Niri) {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.init(niri),
|
||||
Backend::Winit(winit) => winit.init(niri),
|
||||
Backend::Headless(headless) => headless.init(niri),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +67,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.seat_name(),
|
||||
Backend::Winit(winit) => winit.seat_name(),
|
||||
Backend::Headless(headless) => headless.seat_name(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +78,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.with_primary_renderer(f),
|
||||
Backend::Winit(winit) => winit.with_primary_renderer(f),
|
||||
Backend::Headless(headless) => headless.with_primary_renderer(f),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,13 +91,20 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.render(niri, output, target_presentation_time),
|
||||
Backend::Winit(winit) => winit.render(niri, output),
|
||||
Backend::Headless(headless) => headless.render(niri, output),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mod_key(&self) -> CompositorMod {
|
||||
pub fn mod_key(&self, config: &Config) -> ModKey {
|
||||
match self {
|
||||
Backend::Tty(_) => CompositorMod::Super,
|
||||
Backend::Winit(_) => CompositorMod::Alt,
|
||||
Backend::Winit(_) => config.input.mod_key_nested.unwrap_or({
|
||||
if let Some(ModKey::Alt) = config.input.mod_key {
|
||||
ModKey::Super
|
||||
} else {
|
||||
ModKey::Alt
|
||||
}
|
||||
}),
|
||||
Backend::Tty(_) | Backend::Headless(_) => config.input.mod_key.unwrap_or(ModKey::Super),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +112,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.change_vt(vt),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +120,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.suspend(),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +128,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.toggle_debug_tint(),
|
||||
Backend::Winit(winit) => winit.toggle_debug_tint(),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +136,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
|
||||
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
|
||||
Backend::Headless(headless) => headless.import_dmabuf(dmabuf),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +144,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.early_import(surface),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +152,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.ipc_outputs(),
|
||||
Backend::Winit(winit) => winit.ipc_outputs(),
|
||||
Backend::Headless(headless) => headless.ipc_outputs(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +164,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.primary_gbm_device(),
|
||||
Backend::Winit(_) => None,
|
||||
Backend::Headless(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +172,15 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.set_monitors_active(active),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_output_on_demand_vrr(&mut self, niri: &mut Niri, output: &Output, enable_vrr: bool) {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.set_output_on_demand_vrr(niri, output, enable_vrr),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,13 +188,15 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.on_output_config_changed(niri),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_debug_config_changed(&mut self) {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.on_debug_config_changed(),
|
||||
Backend::Winit(_) => (),
|
||||
pub fn tty_checked(&mut self) -> Option<&mut Tty> {
|
||||
if let Self::Tty(v) = self {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,4 +215,12 @@ impl Backend {
|
||||
panic!("backend is not Winit")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn headless(&mut self) -> &mut Headless {
|
||||
if let Self::Headless(v) = self {
|
||||
v
|
||||
} else {
|
||||
panic!("backend is not Headless")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+554
-348
File diff suppressed because it is too large
Load Diff
+28
-18
@@ -3,9 +3,8 @@ use std::collections::HashMap;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::Config;
|
||||
use niri_config::{Config, OutputName};
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
@@ -15,9 +14,10 @@ use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||
use smithay::reexports::calloop::LoopHandle;
|
||||
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||
use smithay::reexports::winit::dpi::LogicalSize;
|
||||
use smithay::reexports::winit::window::WindowBuilder;
|
||||
use smithay::reexports::winit::window::Window;
|
||||
use smithay::wayland::presentation::Refresh;
|
||||
|
||||
use super::{IpcOutputMap, RenderResult};
|
||||
use super::{IpcOutputMap, OutputId, RenderResult};
|
||||
use crate::niri::{Niri, RedrawState, State};
|
||||
use crate::render_helpers::debug::draw_damage;
|
||||
use crate::render_helpers::{resources, shaders, RenderTarget};
|
||||
@@ -36,11 +36,11 @@ impl Winit {
|
||||
config: Rc<RefCell<Config>>,
|
||||
event_loop: LoopHandle<State>,
|
||||
) -> Result<Self, winit::Error> {
|
||||
let builder = WindowBuilder::new()
|
||||
let builder = Window::default_attributes()
|
||||
.with_inner_size(LogicalSize::new(1280.0, 800.0))
|
||||
// .with_resizable(false)
|
||||
.with_title("niri");
|
||||
let (backend, winit) = winit::init_from_builder(builder)?;
|
||||
let (backend, winit) = winit::init_from_attributes(builder)?;
|
||||
|
||||
let output = Output::new(
|
||||
"winit".to_string(),
|
||||
@@ -59,13 +59,21 @@ impl Winit {
|
||||
output.change_current_state(Some(mode), None, None, None);
|
||||
output.set_preferred(mode);
|
||||
|
||||
output.user_data().insert_if_missing(|| OutputName {
|
||||
connector: "winit".to_string(),
|
||||
make: Some("Smithay".to_string()),
|
||||
model: Some("Winit".to_string()),
|
||||
serial: None,
|
||||
});
|
||||
|
||||
let physical_properties = output.physical_properties();
|
||||
let ipc_outputs = Arc::new(Mutex::new(HashMap::from([(
|
||||
"winit".to_owned(),
|
||||
OutputId::next(),
|
||||
niri_ipc::Output {
|
||||
name: output.name(),
|
||||
make: physical_properties.make,
|
||||
model: physical_properties.model,
|
||||
serial: None,
|
||||
physical_size: None,
|
||||
modes: vec![niri_ipc::Mode {
|
||||
width: backend.window_size().w.clamp(0, u16::MAX as i32) as u16,
|
||||
@@ -98,7 +106,7 @@ impl Winit {
|
||||
|
||||
{
|
||||
let mut ipc_outputs = winit.ipc_outputs.lock().unwrap();
|
||||
let output = ipc_outputs.get_mut("winit").unwrap();
|
||||
let output = ipc_outputs.values_mut().next().unwrap();
|
||||
let mode = &mut output.modes[0];
|
||||
mode.width = size.w.clamp(0, u16::MAX as i32) as u16;
|
||||
mode.height = size.h.clamp(0, u16::MAX as i32) as u16;
|
||||
@@ -148,7 +156,7 @@ impl Winit {
|
||||
}
|
||||
drop(config);
|
||||
|
||||
niri.layout.update_shaders();
|
||||
niri.update_shaders();
|
||||
|
||||
niri.add_output(self.output.clone(), None, false);
|
||||
}
|
||||
@@ -182,12 +190,16 @@ impl Winit {
|
||||
}
|
||||
|
||||
// Hand them over to winit.
|
||||
self.backend.bind().unwrap();
|
||||
let age = self.backend.buffer_age().unwrap();
|
||||
let res = self
|
||||
.damage_tracker
|
||||
.render_output(self.backend.renderer(), age, &elements, [0.; 4])
|
||||
.unwrap();
|
||||
let res = {
|
||||
let (renderer, mut framebuffer) = self.backend.bind().unwrap();
|
||||
// FIXME: currently impossible to call due to a mutable borrow.
|
||||
//
|
||||
// let age = self.backend.buffer_age().unwrap();
|
||||
let age = 0;
|
||||
self.damage_tracker
|
||||
.render_output(renderer, &mut framebuffer, age, &elements, [0.; 4])
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
niri.update_primary_scanout_output(output, &res.states);
|
||||
|
||||
@@ -208,11 +220,9 @@ impl Winit {
|
||||
self.backend.submit(Some(damage)).unwrap();
|
||||
|
||||
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &res.states);
|
||||
let mode = output.current_mode().unwrap();
|
||||
let refresh = Duration::from_secs_f64(1_000f64 / mode.refresh as f64);
|
||||
presentation_feedbacks.presented::<_, smithay::utils::Monotonic>(
|
||||
get_monotonic_time(),
|
||||
refresh,
|
||||
Refresh::Unknown,
|
||||
0,
|
||||
wp_presentation_feedback::Kind::empty(),
|
||||
);
|
||||
|
||||
+25
@@ -2,6 +2,7 @@ use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap_complete::Shell;
|
||||
use niri_ipc::{Action, OutputAction};
|
||||
|
||||
use crate::utils::version;
|
||||
@@ -13,6 +14,9 @@ use crate::utils::version;
|
||||
#[command(subcommand_help_heading = "Subcommands")]
|
||||
pub struct Cli {
|
||||
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||
///
|
||||
/// This can also be set with the `NIRI_CONFIG` environment variable. If both are set, the
|
||||
/// command line argument takes precedence.
|
||||
#[arg(short, long)]
|
||||
pub config: Option<PathBuf>,
|
||||
/// Import environment globally to systemd and D-Bus, run D-Bus services.
|
||||
@@ -43,11 +47,16 @@ pub enum Sub {
|
||||
/// Validate the config file.
|
||||
Validate {
|
||||
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||
///
|
||||
/// This can also be set with the `NIRI_CONFIG` environment variable. If both are set, the
|
||||
/// command line argument takes precedence.
|
||||
#[arg(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
},
|
||||
/// Cause a panic to check if the backtraces are good.
|
||||
Panic,
|
||||
/// Generate shell completions.
|
||||
Completions { shell: Shell },
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -56,8 +65,20 @@ pub enum Msg {
|
||||
Outputs,
|
||||
/// List workspaces.
|
||||
Workspaces,
|
||||
/// List open windows.
|
||||
Windows,
|
||||
/// List open layer-shell surfaces.
|
||||
Layers,
|
||||
/// Get the configured keyboard layouts.
|
||||
KeyboardLayouts,
|
||||
/// Print information about the focused output.
|
||||
FocusedOutput,
|
||||
/// Print information about the focused window.
|
||||
FocusedWindow,
|
||||
/// Pick a window with the mouse and print information about it.
|
||||
PickWindow,
|
||||
/// Pick a color from the screen with the mouse.
|
||||
PickColor,
|
||||
/// Perform an action.
|
||||
Action {
|
||||
#[command(subcommand)]
|
||||
@@ -78,8 +99,12 @@ pub enum Msg {
|
||||
#[command(subcommand)]
|
||||
action: OutputAction,
|
||||
},
|
||||
/// Start continuously receiving events from the compositor.
|
||||
EventStream,
|
||||
/// Print the version of the running niri instance.
|
||||
Version,
|
||||
/// Request an error from the running niri instance.
|
||||
RequestError,
|
||||
/// Print the overview state.
|
||||
OverviewState,
|
||||
}
|
||||
|
||||
+18
-16
@@ -4,12 +4,11 @@ use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus, CursorImageSurfaceData};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
@@ -67,7 +66,7 @@ impl CursorManager {
|
||||
let hotspot = with_states(&surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<Mutex<CursorImageAttributes>>()
|
||||
.get::<CursorImageSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
@@ -76,21 +75,24 @@ impl CursorManager {
|
||||
|
||||
RenderCursor::Surface { hotspot, surface }
|
||||
}
|
||||
CursorImageStatus::Named(icon) => self
|
||||
.get_cursor_with_name(icon, scale)
|
||||
.map(|cursor| RenderCursor::Named {
|
||||
icon,
|
||||
scale,
|
||||
cursor,
|
||||
})
|
||||
.unwrap_or_else(|| RenderCursor::Named {
|
||||
icon: Default::default(),
|
||||
scale,
|
||||
cursor: self.get_default_cursor(scale),
|
||||
}),
|
||||
CursorImageStatus::Named(icon) => self.get_render_cursor_named(icon, scale),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_render_cursor_named(&self, icon: CursorIcon, scale: i32) -> RenderCursor {
|
||||
self.get_cursor_with_name(icon, scale)
|
||||
.map(|cursor| RenderCursor::Named {
|
||||
icon,
|
||||
scale,
|
||||
cursor,
|
||||
})
|
||||
.unwrap_or_else(|| RenderCursor::Named {
|
||||
icon: Default::default(),
|
||||
scale,
|
||||
cursor: self.get_default_cursor(scale),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_current_cursor_animated(&self, scale: i32) -> bool {
|
||||
match &self.current_cursor {
|
||||
CursorImageStatus::Hidden => false,
|
||||
@@ -142,7 +144,7 @@ impl CursorManager {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Currenly used cursor_image as a cursor provider.
|
||||
/// Currently used cursor_image as a cursor provider.
|
||||
pub fn cursor_image(&self) -> &CursorImageStatus {
|
||||
&self.current_cursor
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ use std::sync::{Arc, Mutex, OnceLock};
|
||||
use anyhow::Context;
|
||||
use futures_util::StreamExt;
|
||||
use zbus::fdo::{self, RequestNameFlags};
|
||||
use zbus::message::Header;
|
||||
use zbus::names::{OwnedUniqueName, UniqueName};
|
||||
use zbus::zvariant::NoneValue;
|
||||
use zbus::{dbus_interface, MessageHeader, Task};
|
||||
use zbus::{interface, Task};
|
||||
|
||||
use super::Start;
|
||||
|
||||
@@ -20,11 +21,11 @@ pub struct ScreenSaver {
|
||||
monitor_task: Arc<OnceLock<Task<()>>>,
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.freedesktop.ScreenSaver")]
|
||||
#[interface(name = "org.freedesktop.ScreenSaver")]
|
||||
impl ScreenSaver {
|
||||
async fn inhibit(
|
||||
&mut self,
|
||||
#[zbus(header)] hdr: MessageHeader<'_>,
|
||||
#[zbus(header)] hdr: Header<'_>,
|
||||
application_name: &str,
|
||||
reason_for_inhibit: &str,
|
||||
) -> fdo::Result<u32> {
|
||||
@@ -33,7 +34,7 @@ impl ScreenSaver {
|
||||
hdr.sender()
|
||||
);
|
||||
|
||||
let Ok(Some(name)) = hdr.sender() else {
|
||||
let Some(name) = hdr.sender() else {
|
||||
return Err(fdo::Error::Failed(String::from("no sender")));
|
||||
};
|
||||
let name = OwnedUniqueName::from(name.to_owned());
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use zbus::fdo::{self, RequestNameFlags};
|
||||
use zbus::interface;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::{SerializeDict, Type, Value};
|
||||
|
||||
use super::Start;
|
||||
|
||||
pub struct Introspect {
|
||||
to_niri: calloop::channel::Sender<IntrospectToNiri>,
|
||||
from_niri: async_channel::Receiver<NiriToIntrospect>,
|
||||
}
|
||||
|
||||
pub enum IntrospectToNiri {
|
||||
GetWindows,
|
||||
}
|
||||
|
||||
pub enum NiriToIntrospect {
|
||||
Windows(HashMap<u64, WindowProperties>),
|
||||
}
|
||||
|
||||
#[derive(Debug, SerializeDict, Type, Value)]
|
||||
#[zvariant(signature = "dict")]
|
||||
pub struct WindowProperties {
|
||||
/// Window title.
|
||||
pub title: String,
|
||||
/// Window app ID.
|
||||
///
|
||||
/// This is actually the name of the .desktop file, and Shell does internal tracking to match
|
||||
/// Wayland app IDs to desktop files. We don't do that yet, which is the reason why
|
||||
/// xdg-desktop-portal-gnome's window list is missing icons.
|
||||
#[zvariant(rename = "app-id")]
|
||||
pub app_id: String,
|
||||
}
|
||||
|
||||
#[interface(name = "org.gnome.Shell.Introspect")]
|
||||
impl Introspect {
|
||||
async fn get_windows(&self) -> fdo::Result<HashMap<u64, WindowProperties>> {
|
||||
if let Err(err) = self.to_niri.send(IntrospectToNiri::GetWindows) {
|
||||
warn!("error sending message to niri: {err:?}");
|
||||
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||
}
|
||||
|
||||
match self.from_niri.recv().await {
|
||||
Ok(NiriToIntrospect::Windows(windows)) => Ok(windows),
|
||||
Err(err) => {
|
||||
warn!("error receiving message from niri: {err:?}");
|
||||
Err(fdo::Error::Failed("internal error".to_owned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: call this upon window changes, once more of the infrastructure is there (will be
|
||||
// needed for the event stream IPC anyway).
|
||||
#[zbus(signal)]
|
||||
pub async fn windows_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
impl Introspect {
|
||||
pub fn new(
|
||||
to_niri: calloop::channel::Sender<IntrospectToNiri>,
|
||||
from_niri: async_channel::Receiver<NiriToIntrospect>,
|
||||
) -> Self {
|
||||
Self { to_niri, from_niri }
|
||||
}
|
||||
}
|
||||
|
||||
impl Start for Introspect {
|
||||
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
|
||||
let conn = zbus::blocking::Connection::session()?;
|
||||
let flags = RequestNameFlags::AllowReplacement
|
||||
| RequestNameFlags::ReplaceExisting
|
||||
| RequestNameFlags::DoNotQueue;
|
||||
|
||||
conn.object_server()
|
||||
.at("/org/gnome/Shell/Introspect", self)?;
|
||||
conn.request_name_with_flags("org.gnome.Shell.Introspect", flags)?;
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use zbus::dbus_interface;
|
||||
use niri_ipc::PickedColor;
|
||||
use zbus::fdo::{self, RequestNameFlags};
|
||||
use zbus::zvariant::OwnedValue;
|
||||
use zbus::{interface, zvariant};
|
||||
|
||||
use super::Start;
|
||||
|
||||
@@ -12,13 +15,14 @@ pub struct Screenshot {
|
||||
|
||||
pub enum ScreenshotToNiri {
|
||||
TakeScreenshot { include_cursor: bool },
|
||||
PickColor(async_channel::Sender<Option<PickedColor>>),
|
||||
}
|
||||
|
||||
pub enum NiriToScreenshot {
|
||||
ScreenshotResult(Option<PathBuf>),
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.gnome.Shell.Screenshot")]
|
||||
#[interface(name = "org.gnome.Shell.Screenshot")]
|
||||
impl Screenshot {
|
||||
async fn screenshot(
|
||||
&self,
|
||||
@@ -47,6 +51,34 @@ impl Screenshot {
|
||||
|
||||
Ok((true, filename))
|
||||
}
|
||||
|
||||
async fn pick_color(&self) -> fdo::Result<HashMap<String, OwnedValue>> {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
if let Err(err) = self.to_niri.send(ScreenshotToNiri::PickColor(tx)) {
|
||||
warn!("error sending pick color message to niri: {err:?}");
|
||||
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||
}
|
||||
|
||||
let color = match rx.recv().await {
|
||||
Ok(Some(color)) => color,
|
||||
Ok(None) => {
|
||||
return Err(fdo::Error::Failed("no color picked".to_owned()));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error receiving message from niri: {err:?}");
|
||||
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||
}
|
||||
};
|
||||
|
||||
let mut result = HashMap::new();
|
||||
let [r, g, b] = color.rgb;
|
||||
result.insert(
|
||||
"color".to_string(),
|
||||
zvariant::OwnedValue::try_from(zvariant::Value::from((r, g, b))).unwrap(),
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Screenshot {
|
||||
|
||||
+48
-10
@@ -1,9 +1,10 @@
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::Interface;
|
||||
use zbus::object_server::Interface;
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub mod freedesktop_screensaver;
|
||||
pub mod gnome_shell_introspect;
|
||||
pub mod gnome_shell_screenshot;
|
||||
pub mod mutter_display_config;
|
||||
pub mod mutter_service_channel;
|
||||
@@ -14,6 +15,7 @@ pub mod mutter_screen_cast;
|
||||
use mutter_screen_cast::ScreenCast;
|
||||
|
||||
use self::freedesktop_screensaver::ScreenSaver;
|
||||
use self::gnome_shell_introspect::Introspect;
|
||||
use self::mutter_display_config::DisplayConfig;
|
||||
use self::mutter_service_channel::ServiceChannel;
|
||||
|
||||
@@ -27,6 +29,7 @@ pub struct DBusServers {
|
||||
pub conn_display_config: Option<Connection>,
|
||||
pub conn_screen_saver: Option<Connection>,
|
||||
pub conn_screen_shot: Option<Connection>,
|
||||
pub conn_introspect: Option<Connection>,
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
pub conn_screen_cast: Option<Connection>,
|
||||
}
|
||||
@@ -42,12 +45,39 @@ impl DBusServers {
|
||||
let mut dbus = Self::default();
|
||||
|
||||
if is_session_instance {
|
||||
let service_channel = ServiceChannel::new(niri.display_handle.clone());
|
||||
let (to_niri, from_service_channel) = calloop::channel::channel();
|
||||
let service_channel = ServiceChannel::new(to_niri);
|
||||
niri.event_loop
|
||||
.insert_source(from_service_channel, move |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(new_client) => {
|
||||
state.niri.insert_client(new_client);
|
||||
}
|
||||
calloop::channel::Event::Closed => (),
|
||||
})
|
||||
.unwrap();
|
||||
dbus.conn_service_channel = try_start(service_channel);
|
||||
}
|
||||
|
||||
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
|
||||
let display_config = DisplayConfig::new(backend.ipc_outputs());
|
||||
let (to_niri, from_display_config) = calloop::channel::channel();
|
||||
let display_config = DisplayConfig::new(to_niri, backend.ipc_outputs());
|
||||
niri.event_loop
|
||||
.insert_source(from_display_config, move |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(new_conf) => {
|
||||
for (name, conf) in new_conf {
|
||||
state.modify_output_config(&name, move |output| {
|
||||
if let Some(new_output) = conf {
|
||||
*output = new_output;
|
||||
} else {
|
||||
output.off = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
state.reload_output_config();
|
||||
}
|
||||
calloop::channel::Event::Closed => (),
|
||||
})
|
||||
.unwrap();
|
||||
dbus.conn_display_config = try_start(display_config);
|
||||
|
||||
let screen_saver = ScreenSaver::new(niri.is_fdo_idle_inhibited.clone());
|
||||
@@ -66,24 +96,32 @@ impl DBusServers {
|
||||
let screenshot = gnome_shell_screenshot::Screenshot::new(to_niri, from_niri);
|
||||
dbus.conn_screen_shot = try_start(screenshot);
|
||||
|
||||
let (to_niri, from_introspect) = calloop::channel::channel();
|
||||
let (to_introspect, from_niri) = async_channel::unbounded();
|
||||
niri.event_loop
|
||||
.insert_source(from_introspect, move |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(msg) => {
|
||||
state.on_introspect_msg(&to_introspect, msg)
|
||||
}
|
||||
calloop::channel::Event::Closed => (),
|
||||
})
|
||||
.unwrap();
|
||||
let introspect = Introspect::new(to_niri, from_niri);
|
||||
dbus.conn_introspect = try_start(introspect);
|
||||
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
if niri.pipewire.is_some() {
|
||||
{
|
||||
let (to_niri, from_screen_cast) = calloop::channel::channel();
|
||||
niri.event_loop
|
||||
.insert_source(from_screen_cast, {
|
||||
let to_niri = to_niri.clone();
|
||||
move |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(msg) => {
|
||||
state.on_screen_cast_msg(&to_niri, msg)
|
||||
}
|
||||
calloop::channel::Event::Msg(msg) => state.on_screen_cast_msg(msg),
|
||||
calloop::channel::Event::Closed => (),
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let screen_cast = ScreenCast::new(backend.ipc_outputs(), to_niri);
|
||||
dbus.conn_screen_cast = try_start(screen_cast);
|
||||
} else {
|
||||
warn!("disabling screencast support because we couldn't start PipeWire");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smithay::utils::Size;
|
||||
use zbus::fdo::RequestNameFlags;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::{self, OwnedValue, Type};
|
||||
use zbus::{dbus_interface, fdo, SignalContext};
|
||||
use zbus::{fdo, interface};
|
||||
|
||||
use super::Start;
|
||||
use crate::backend::IpcOutputMap;
|
||||
use crate::utils::is_laptop_panel;
|
||||
use crate::utils::scale::supported_scales;
|
||||
|
||||
pub struct DisplayConfig {
|
||||
to_niri: calloop::channel::Sender<HashMap<String, Option<niri_config::Output>>>,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
}
|
||||
|
||||
@@ -42,7 +48,18 @@ pub struct LogicalMonitor {
|
||||
properties: HashMap<String, OwnedValue>,
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.gnome.Mutter.DisplayConfig")]
|
||||
// ApplyMonitorsConfig
|
||||
#[derive(Deserialize, Type)]
|
||||
pub struct LogicalMonitorConfiguration {
|
||||
x: i32,
|
||||
y: i32,
|
||||
scale: f64,
|
||||
transform: u32,
|
||||
_is_primary: bool,
|
||||
monitors: Vec<(String, String, HashMap<String, OwnedValue>)>,
|
||||
}
|
||||
|
||||
#[interface(name = "org.gnome.Mutter.DisplayConfig")]
|
||||
impl DisplayConfig {
|
||||
async fn get_current_state(
|
||||
&self,
|
||||
@@ -53,71 +70,70 @@ impl DisplayConfig {
|
||||
HashMap<String, OwnedValue>,
|
||||
)> {
|
||||
// Construct the DBus response.
|
||||
let mut monitors: Vec<(Monitor, LogicalMonitor)> = self
|
||||
.ipc_outputs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
// Take only enabled outputs.
|
||||
.filter(|(_, output)| output.current_mode.is_some() && output.logical.is_some())
|
||||
.map(|(c, output)| {
|
||||
// Loosely matches the check in Mutter.
|
||||
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
|
||||
let mut monitors = Vec::new();
|
||||
let mut logical_monitors = Vec::new();
|
||||
|
||||
// FIXME: use proper serial when we have libdisplay-info.
|
||||
// A serial is required for correct session restore by xdp-gnome.
|
||||
let serial = c.clone();
|
||||
for output in self.ipc_outputs.lock().unwrap().values() {
|
||||
// Loosely matches the check in Mutter.
|
||||
let c = &output.name;
|
||||
let is_laptop_panel = is_laptop_panel(c);
|
||||
let display_name = make_display_name(output, is_laptop_panel);
|
||||
|
||||
let mut properties = HashMap::new();
|
||||
if is_laptop_panel {
|
||||
properties.insert(
|
||||
String::from("display-name"),
|
||||
OwnedValue::from(zvariant::Str::from_static("Built-in display")),
|
||||
);
|
||||
}
|
||||
properties.insert(
|
||||
String::from("is-builtin"),
|
||||
OwnedValue::from(is_laptop_panel),
|
||||
);
|
||||
let mut properties = HashMap::new();
|
||||
properties.insert(
|
||||
String::from("display-name"),
|
||||
OwnedValue::from(zvariant::Str::from(display_name)),
|
||||
);
|
||||
properties.insert(
|
||||
String::from("is-builtin"),
|
||||
OwnedValue::from(is_laptop_panel),
|
||||
);
|
||||
|
||||
let mut modes: Vec<Mode> = output
|
||||
.modes
|
||||
.iter()
|
||||
.map(|m| {
|
||||
let niri_ipc::Mode {
|
||||
width,
|
||||
height,
|
||||
refresh_rate,
|
||||
is_preferred,
|
||||
} = *m;
|
||||
let refresh = refresh_rate as f64 / 1000.;
|
||||
let mut modes: Vec<Mode> = output
|
||||
.modes
|
||||
.iter()
|
||||
.map(|m| {
|
||||
let niri_ipc::Mode {
|
||||
width,
|
||||
height,
|
||||
refresh_rate,
|
||||
is_preferred,
|
||||
} = *m;
|
||||
let width = i32::from(width);
|
||||
let height = i32::from(height);
|
||||
let refresh_rate = refresh_rate as f64 / 1000.;
|
||||
|
||||
Mode {
|
||||
id: format!("{width}x{height}@{refresh:.3}"),
|
||||
width: i32::from(width),
|
||||
height: i32::from(height),
|
||||
refresh_rate: refresh,
|
||||
preferred_scale: 1.,
|
||||
supported_scales: vec![1., 2., 3.],
|
||||
properties: HashMap::from([(
|
||||
String::from("is-preferred"),
|
||||
OwnedValue::from(is_preferred),
|
||||
)]),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
modes[output.current_mode.unwrap()]
|
||||
Mode {
|
||||
id: format!("{width}x{height}@{refresh_rate:.3}"),
|
||||
width,
|
||||
height,
|
||||
refresh_rate,
|
||||
preferred_scale: 1.,
|
||||
supported_scales: supported_scales(Size::from((width, height))).collect(),
|
||||
properties: HashMap::from([(
|
||||
String::from("is-preferred"),
|
||||
OwnedValue::from(is_preferred),
|
||||
)]),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if let Some(mode) = output.current_mode {
|
||||
modes[mode]
|
||||
.properties
|
||||
.insert(String::from("is-current"), OwnedValue::from(true));
|
||||
}
|
||||
|
||||
let monitor = Monitor {
|
||||
names: (c.clone(), String::new(), String::new(), serial),
|
||||
modes,
|
||||
properties,
|
||||
};
|
||||
let connector = c.clone();
|
||||
let model = output.model.clone();
|
||||
let make = output.make.clone();
|
||||
|
||||
let logical = output.logical.as_ref().unwrap();
|
||||
// Serial is used for session restore, so fall back to the connector name if it's
|
||||
// not available.
|
||||
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
|
||||
|
||||
let names = (connector, make, model, serial);
|
||||
|
||||
if let Some(logical) = output.logical.as_ref() {
|
||||
let transform = match logical.transform {
|
||||
niri_ipc::Transform::Normal => 0,
|
||||
niri_ipc::Transform::_90 => 1,
|
||||
@@ -129,42 +145,151 @@ impl DisplayConfig {
|
||||
niri_ipc::Transform::Flipped270 => 7,
|
||||
};
|
||||
|
||||
let logical_monitor = LogicalMonitor {
|
||||
logical_monitors.push(LogicalMonitor {
|
||||
x: logical.x,
|
||||
y: logical.y,
|
||||
scale: logical.scale,
|
||||
transform,
|
||||
is_primary: false,
|
||||
monitors: vec![monitor.names.clone()],
|
||||
monitors: vec![names.clone()],
|
||||
properties: HashMap::new(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
(monitor, logical_monitor)
|
||||
})
|
||||
.collect();
|
||||
monitors.push(Monitor {
|
||||
names,
|
||||
modes,
|
||||
properties,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort the built-in monitor first, then by connector name.
|
||||
monitors.sort_unstable_by(|a, b| {
|
||||
let a_is_builtin = a.0.properties.contains_key("display-name");
|
||||
let b_is_builtin = b.0.properties.contains_key("display-name");
|
||||
a_is_builtin
|
||||
.cmp(&b_is_builtin)
|
||||
.reverse()
|
||||
.then_with(|| a.0.names.0.cmp(&b.0.names.0))
|
||||
});
|
||||
// Sort by connector.
|
||||
monitors.sort_unstable_by(|a, b| a.names.0.cmp(&b.names.0));
|
||||
logical_monitors.sort_unstable_by(|a, b| a.monitors[0].0.cmp(&b.monitors[0].0));
|
||||
|
||||
let (monitors, logical_monitors) = monitors.into_iter().unzip();
|
||||
let properties = HashMap::from([(String::from("layout-mode"), OwnedValue::from(1u32))]);
|
||||
Ok((0, monitors, logical_monitors, properties))
|
||||
}
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
|
||||
async fn apply_monitors_config(
|
||||
&self,
|
||||
_serial: u32,
|
||||
method: u32,
|
||||
logical_monitor_configs: Vec<LogicalMonitorConfiguration>,
|
||||
_properties: HashMap<String, OwnedValue>,
|
||||
) -> fdo::Result<()> {
|
||||
let current_conf = self.ipc_outputs.lock().unwrap();
|
||||
let mut new_conf = HashMap::new();
|
||||
for requested_config in logical_monitor_configs {
|
||||
if requested_config.monitors.len() > 1 {
|
||||
return Err(zbus::fdo::Error::Failed(
|
||||
"Mirroring is not yet supported".to_owned(),
|
||||
));
|
||||
}
|
||||
for (connector, mode, _props) in requested_config.monitors {
|
||||
if !current_conf.values().any(|o| o.name == connector) {
|
||||
return Err(zbus::fdo::Error::Failed(format!(
|
||||
"Connector '{}' not found",
|
||||
connector
|
||||
)));
|
||||
}
|
||||
new_conf.insert(
|
||||
connector.clone(),
|
||||
Some(niri_config::Output {
|
||||
off: false,
|
||||
name: connector,
|
||||
scale: Some(niri_config::FloatOrInt(requested_config.scale)),
|
||||
transform: match requested_config.transform {
|
||||
0 => niri_ipc::Transform::Normal,
|
||||
1 => niri_ipc::Transform::_90,
|
||||
2 => niri_ipc::Transform::_180,
|
||||
3 => niri_ipc::Transform::_270,
|
||||
4 => niri_ipc::Transform::Flipped,
|
||||
5 => niri_ipc::Transform::Flipped90,
|
||||
6 => niri_ipc::Transform::Flipped180,
|
||||
7 => niri_ipc::Transform::Flipped270,
|
||||
x => {
|
||||
return Err(zbus::fdo::Error::Failed(format!(
|
||||
"Unknown transform {}",
|
||||
x
|
||||
)))
|
||||
}
|
||||
},
|
||||
position: Some(niri_config::Position {
|
||||
x: requested_config.x,
|
||||
y: requested_config.y,
|
||||
}),
|
||||
mode: Some(niri_ipc::ConfiguredMode::from_str(&mode).map_err(|e| {
|
||||
zbus::fdo::Error::Failed(format!(
|
||||
"Could not parse mode '{}': {}",
|
||||
mode, e
|
||||
))
|
||||
})?),
|
||||
// FIXME: VRR
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
if new_conf.is_empty() {
|
||||
return Err(zbus::fdo::Error::Failed(
|
||||
"At least one output must be enabled".to_owned(),
|
||||
));
|
||||
}
|
||||
for output in current_conf.values() {
|
||||
if !new_conf.contains_key(&output.name) {
|
||||
new_conf.insert(output.name.clone(), None);
|
||||
}
|
||||
}
|
||||
if method == 0 {
|
||||
// 0 means "verify", so don't actually apply here
|
||||
return Ok(());
|
||||
}
|
||||
if let Err(err) = self.to_niri.send(new_conf) {
|
||||
warn!("error sending message to niri: {err:?}");
|
||||
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[zbus(signal)]
|
||||
pub async fn monitors_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn power_save_mode(&self) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
fn set_power_save_mode(&self, _mode: i32) -> zbus::Result<()> {
|
||||
Err(zbus::Error::Unsupported)
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
fn panel_orientation_managed(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
fn apply_monitors_config_allowed(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
fn night_light_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayConfig {
|
||||
pub fn new(ipc_outputs: Arc<Mutex<IpcOutputMap>>) -> Self {
|
||||
Self { ipc_outputs }
|
||||
pub fn new(
|
||||
to_niri: calloop::channel::Sender<HashMap<String, Option<niri_config::Output>>>,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
to_niri,
|
||||
ipc_outputs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,3 +307,48 @@ impl Start for DisplayConfig {
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from Mutter.
|
||||
fn make_display_name(output: &niri_ipc::Output, is_laptop_panel: bool) -> String {
|
||||
if is_laptop_panel {
|
||||
return String::from("Built-in display");
|
||||
}
|
||||
|
||||
let make = &output.make;
|
||||
let model = &output.model;
|
||||
if let Some(diagonal) = output.physical_size.map(|(width_mm, height_mm)| {
|
||||
let diagonal = f64::hypot(f64::from(width_mm), f64::from(height_mm)) / 25.4;
|
||||
format_diagonal(diagonal)
|
||||
}) {
|
||||
format!("{make} {diagonal}")
|
||||
} else if model != "Unknown" {
|
||||
format!("{make} {model}")
|
||||
} else {
|
||||
make.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn format_diagonal(diagonal_inches: f64) -> String {
|
||||
let known = [12.1, 13.3, 15.6];
|
||||
if let Some(d) = known.iter().find(|d| (*d - diagonal_inches).abs() < 0.1) {
|
||||
format!("{d:.1}″")
|
||||
} else {
|
||||
format!("{}″", diagonal_inches.round() as u32)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_diagonal() {
|
||||
assert_snapshot!(format_diagonal(12.11), @"12.1″");
|
||||
assert_snapshot!(format_diagonal(13.28), @"13.3″");
|
||||
assert_snapshot!(format_diagonal(15.6), @"15.6″");
|
||||
assert_snapshot!(format_diagonal(23.2), @"23″");
|
||||
assert_snapshot!(format_diagonal(24.8), @"25″");
|
||||
}
|
||||
}
|
||||
|
||||
+137
-36
@@ -4,10 +4,10 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::Deserialize;
|
||||
use smithay::output::Output;
|
||||
use zbus::fdo::RequestNameFlags;
|
||||
use zbus::object_server::{InterfaceRef, SignalEmitter};
|
||||
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
|
||||
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
|
||||
use zbus::{fdo, interface, ObjectServer};
|
||||
|
||||
use super::Start;
|
||||
use crate::backend::IpcOutputMap;
|
||||
@@ -47,15 +47,42 @@ struct RecordMonitorProperties {
|
||||
_is_recording: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, DeserializeDict, Type)]
|
||||
#[zvariant(signature = "dict")]
|
||||
struct RecordWindowProperties {
|
||||
#[zvariant(rename = "window-id")]
|
||||
window_id: u64,
|
||||
#[zvariant(rename = "cursor-mode")]
|
||||
cursor_mode: Option<CursorMode>,
|
||||
#[zvariant(rename = "is-recording")]
|
||||
_is_recording: Option<bool>,
|
||||
}
|
||||
|
||||
static STREAM_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Stream {
|
||||
// FIXME: update on scale changes and whatnot.
|
||||
output: niri_ipc::Output,
|
||||
id: usize,
|
||||
session_id: usize,
|
||||
target: StreamTarget,
|
||||
cursor_mode: CursorMode,
|
||||
was_started: Arc<AtomicBool>,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum StreamTarget {
|
||||
// FIXME: update on scale changes and whatnot.
|
||||
Output(niri_ipc::Output),
|
||||
Window { id: u64 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum StreamTargetId {
|
||||
Output { name: String },
|
||||
Window { id: u64 },
|
||||
}
|
||||
|
||||
#[derive(Debug, SerializeDict, Type, Value)]
|
||||
#[zvariant(signature = "dict")]
|
||||
struct StreamParameters {
|
||||
@@ -68,17 +95,17 @@ struct StreamParameters {
|
||||
pub enum ScreenCastToNiri {
|
||||
StartCast {
|
||||
session_id: usize,
|
||||
output: String,
|
||||
stream_id: usize,
|
||||
target: StreamTargetId,
|
||||
cursor_mode: CursorMode,
|
||||
signal_ctx: SignalContext<'static>,
|
||||
signal_ctx: SignalEmitter<'static>,
|
||||
},
|
||||
StopCast {
|
||||
session_id: usize,
|
||||
},
|
||||
Redraw(Output),
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
|
||||
#[interface(name = "org.gnome.Mutter.ScreenCast")]
|
||||
impl ScreenCast {
|
||||
async fn create_session(
|
||||
&self,
|
||||
@@ -113,26 +140,26 @@ impl ScreenCast {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[dbus_interface(property)]
|
||||
#[zbus(property)]
|
||||
async fn version(&self) -> i32 {
|
||||
4
|
||||
}
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Session")]
|
||||
#[interface(name = "org.gnome.Mutter.ScreenCast.Session")]
|
||||
impl Session {
|
||||
async fn start(&self) {
|
||||
debug!("start");
|
||||
|
||||
for (stream, iface) in &*self.streams.lock().unwrap() {
|
||||
stream.start(self.id, iface.signal_context().clone());
|
||||
stream.start(iface.signal_emitter().clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop(
|
||||
&self,
|
||||
#[zbus(object_server)] server: &ObjectServer,
|
||||
#[zbus(signal_context)] ctxt: SignalContext<'_>,
|
||||
#[zbus(signal_context)] ctxt: SignalEmitter<'_>,
|
||||
) {
|
||||
debug!("stop");
|
||||
|
||||
@@ -152,7 +179,7 @@ impl Session {
|
||||
let streams = mem::take(&mut *self.streams.lock().unwrap());
|
||||
for (_, iface) in streams.iter() {
|
||||
server
|
||||
.remove::<Stream, _>(iface.signal_context().path())
|
||||
.remove::<Stream, _>(iface.signal_emitter().path())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -168,7 +195,11 @@ impl Session {
|
||||
) -> fdo::Result<OwnedObjectPath> {
|
||||
debug!(connector, ?properties, "record_monitor");
|
||||
|
||||
let Some(output) = self.ipc_outputs.lock().unwrap().get(connector).cloned() else {
|
||||
let output = {
|
||||
let ipc_outputs = self.ipc_outputs.lock().unwrap();
|
||||
ipc_outputs.values().find(|o| o.name == connector).cloned()
|
||||
};
|
||||
let Some(output) = output else {
|
||||
return Err(fdo::Error::Failed("no such monitor".to_owned()));
|
||||
};
|
||||
|
||||
@@ -176,16 +207,20 @@ impl Session {
|
||||
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
|
||||
}
|
||||
|
||||
static NUMBER: AtomicUsize = AtomicUsize::new(0);
|
||||
let path = format!(
|
||||
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
|
||||
NUMBER.fetch_add(1, Ordering::SeqCst)
|
||||
);
|
||||
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
|
||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||
|
||||
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
||||
|
||||
let stream = Stream::new(output.clone(), cursor_mode, self.to_niri.clone());
|
||||
let target = StreamTarget::Output(output);
|
||||
let stream = Stream::new(
|
||||
stream_id,
|
||||
self.id,
|
||||
target,
|
||||
cursor_mode,
|
||||
self.to_niri.clone(),
|
||||
);
|
||||
match server.at(&path, stream.clone()).await {
|
||||
Ok(true) => {
|
||||
let iface = server.interface(&path).await.unwrap();
|
||||
@@ -202,22 +237,72 @@ impl Session {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
async fn closed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
|
||||
async fn record_window(
|
||||
&mut self,
|
||||
#[zbus(object_server)] server: &ObjectServer,
|
||||
properties: RecordWindowProperties,
|
||||
) -> fdo::Result<OwnedObjectPath> {
|
||||
debug!(?properties, "record_window");
|
||||
|
||||
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
|
||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||
|
||||
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
||||
|
||||
let target = StreamTarget::Window {
|
||||
id: properties.window_id,
|
||||
};
|
||||
let stream = Stream::new(
|
||||
stream_id,
|
||||
self.id,
|
||||
target,
|
||||
cursor_mode,
|
||||
self.to_niri.clone(),
|
||||
);
|
||||
match server.at(&path, stream.clone()).await {
|
||||
Ok(true) => {
|
||||
let iface = server.interface(&path).await.unwrap();
|
||||
self.streams.lock().unwrap().push((stream, iface));
|
||||
}
|
||||
Ok(false) => return Err(fdo::Error::Failed("stream path already exists".to_owned())),
|
||||
Err(err) => {
|
||||
return Err(fdo::Error::Failed(format!(
|
||||
"error creating stream object: {err:?}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[zbus(signal)]
|
||||
async fn closed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Stream")]
|
||||
#[interface(name = "org.gnome.Mutter.ScreenCast.Stream")]
|
||||
impl Stream {
|
||||
#[dbus_interface(signal)]
|
||||
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
|
||||
#[zbus(signal)]
|
||||
pub async fn pipe_wire_stream_added(ctxt: &SignalEmitter<'_>, node_id: u32)
|
||||
-> zbus::Result<()>;
|
||||
|
||||
#[dbus_interface(property)]
|
||||
#[zbus(property)]
|
||||
async fn parameters(&self) -> StreamParameters {
|
||||
let logical = self.output.logical.as_ref().unwrap();
|
||||
StreamParameters {
|
||||
position: (logical.x, logical.y),
|
||||
size: (logical.width as i32, logical.height as i32),
|
||||
match &self.target {
|
||||
StreamTarget::Output(output) => {
|
||||
let logical = output.logical.as_ref().unwrap();
|
||||
StreamParameters {
|
||||
position: (logical.x, logical.y),
|
||||
size: (logical.width as i32, logical.height as i32),
|
||||
}
|
||||
}
|
||||
StreamTarget::Window { .. } => {
|
||||
// Does any consumer need this?
|
||||
StreamParameters {
|
||||
position: (0, 0),
|
||||
size: (1, 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,27 +360,32 @@ impl Drop for Session {
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
pub fn new(
|
||||
output: niri_ipc::Output,
|
||||
fn new(
|
||||
id: usize,
|
||||
session_id: usize,
|
||||
target: StreamTarget,
|
||||
cursor_mode: CursorMode,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
) -> Self {
|
||||
Self {
|
||||
output,
|
||||
id,
|
||||
session_id,
|
||||
target,
|
||||
cursor_mode,
|
||||
was_started: Arc::new(AtomicBool::new(false)),
|
||||
to_niri,
|
||||
}
|
||||
}
|
||||
|
||||
fn start(&self, session_id: usize, ctxt: SignalContext<'static>) {
|
||||
fn start(&self, ctxt: SignalEmitter<'static>) {
|
||||
if self.was_started.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
let msg = ScreenCastToNiri::StartCast {
|
||||
session_id,
|
||||
output: self.output.name.clone(),
|
||||
session_id: self.session_id,
|
||||
stream_id: self.id,
|
||||
target: self.target.make_id(),
|
||||
cursor_mode: self.cursor_mode,
|
||||
signal_ctx: ctxt,
|
||||
};
|
||||
@@ -305,3 +395,14 @@ impl Stream {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamTarget {
|
||||
fn make_id(&self) -> StreamTargetId {
|
||||
match self {
|
||||
StreamTarget::Output(output) => StreamTargetId::Output {
|
||||
name: output.name.clone(),
|
||||
},
|
||||
StreamTarget::Window { id } => StreamTargetId::Window { id: *id },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,51 @@
|
||||
use std::os::fd::{FromRawFd, IntoRawFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::Arc;
|
||||
|
||||
use smithay::reexports::wayland_server::DisplayHandle;
|
||||
use zbus::dbus_interface;
|
||||
use zbus::{fdo, interface, zvariant};
|
||||
|
||||
use super::Start;
|
||||
use crate::niri::ClientState;
|
||||
use crate::niri::NewClient;
|
||||
|
||||
pub struct ServiceChannel {
|
||||
display: DisplayHandle,
|
||||
to_niri: calloop::channel::Sender<NewClient>,
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.gnome.Mutter.ServiceChannel")]
|
||||
#[interface(name = "org.gnome.Mutter.ServiceChannel")]
|
||||
impl ServiceChannel {
|
||||
async fn open_wayland_service_connection(
|
||||
&mut self,
|
||||
service_client_type: u32,
|
||||
) -> zbus::fdo::Result<zbus::zvariant::OwnedFd> {
|
||||
) -> fdo::Result<zvariant::OwnedFd> {
|
||||
if service_client_type != 1 {
|
||||
return Err(zbus::fdo::Error::InvalidArgs(
|
||||
return Err(fdo::Error::InvalidArgs(
|
||||
"Invalid service client type".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
let (sock1, sock2) = UnixStream::pair().unwrap();
|
||||
let data = Arc::new(ClientState {
|
||||
compositor_state: Default::default(),
|
||||
// Would be nice to thread config here but for now it's fine.
|
||||
can_view_decoration_globals: false,
|
||||
let client = NewClient {
|
||||
client: sock2,
|
||||
restricted: false,
|
||||
});
|
||||
self.display.insert_client(sock2, data).unwrap();
|
||||
Ok(unsafe { zbus::zvariant::OwnedFd::from_raw_fd(sock1.into_raw_fd()) })
|
||||
// FIXME: maybe you can get the PID from D-Bus somehow?
|
||||
credentials_unknown: true,
|
||||
};
|
||||
if let Err(err) = self.to_niri.send(client) {
|
||||
warn!("error sending message to niri: {err:?}");
|
||||
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||
}
|
||||
|
||||
Ok(zvariant::OwnedFd::from(std::os::fd::OwnedFd::from(sock1)))
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceChannel {
|
||||
pub fn new(display: DisplayHandle) -> Self {
|
||||
Self { display }
|
||||
pub fn new(to_niri: calloop::channel::Sender<NewClient>) -> Self {
|
||||
Self { to_niri }
|
||||
}
|
||||
}
|
||||
|
||||
impl Start for ServiceChannel {
|
||||
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
|
||||
let conn = zbus::blocking::ConnectionBuilder::session()?
|
||||
let conn = zbus::blocking::connection::Builder::session()?
|
||||
.name("org.gnome.Mutter.ServiceChannel")?
|
||||
.serve_at("/org/gnome/Mutter/ServiceChannel", self)?
|
||||
.build()?;
|
||||
|
||||
@@ -40,6 +40,10 @@ impl FrameClock {
|
||||
self.last_presentation_time = None;
|
||||
}
|
||||
|
||||
pub fn vrr(&self) -> bool {
|
||||
self.vrr
|
||||
}
|
||||
|
||||
pub fn presented(&mut self, presentation_time: Duration) {
|
||||
if presentation_time.is_zero() {
|
||||
// Not interested in these.
|
||||
|
||||
+257
-102
@@ -1,14 +1,15 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
|
||||
use smithay::input::pointer::CursorImageStatus;
|
||||
use niri_ipc::PositionChange;
|
||||
use smithay::backend::renderer::utils::on_commit_buffer_handler;
|
||||
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
|
||||
use smithay::reexports::calloop::Interest;
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::{Client, Resource};
|
||||
use smithay::wayland::buffer::BufferHandler;
|
||||
use smithay::wayland::compositor::{
|
||||
add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface, send_surface_state,
|
||||
add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface, remove_pre_commit_hook,
|
||||
with_states, BufferAssignment, CompositorClientState, CompositorHandler, CompositorState,
|
||||
SurfaceAttributes,
|
||||
};
|
||||
@@ -18,7 +19,11 @@ use smithay::wayland::shm::{ShmHandler, ShmState};
|
||||
use smithay::{delegate_compositor, delegate_shm};
|
||||
|
||||
use super::xdg_shell::add_mapped_toplevel_pre_commit_hook;
|
||||
use crate::niri::{ClientState, State};
|
||||
use crate::handlers::XDG_ACTIVATION_TOKEN_TIMEOUT;
|
||||
use crate::layout::{ActivateWindow, AddWindowTarget};
|
||||
use crate::niri::{CastTarget, ClientState, LockState, State};
|
||||
use crate::utils::transaction::Transaction;
|
||||
use crate::utils::{is_mapped, send_scale_transform};
|
||||
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
|
||||
|
||||
impl CompositorHandler for State {
|
||||
@@ -37,52 +42,21 @@ impl CompositorHandler for State {
|
||||
}
|
||||
|
||||
if let Some(output) = self.niri.output_for_root(&root) {
|
||||
let scale = output.current_scale().integer_scale();
|
||||
let scale = output.current_scale();
|
||||
let transform = output.current_transform();
|
||||
with_states(surface, |data| {
|
||||
send_surface_state(surface, data, scale, transform);
|
||||
send_scale_transform(surface, data, scale, transform);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn new_surface(&mut self, surface: &WlSurface) {
|
||||
add_pre_commit_hook::<Self, _>(surface, move |state, _dh, surface| {
|
||||
let maybe_dmabuf = with_states(surface, |surface_data| {
|
||||
surface_data
|
||||
.cached_state
|
||||
.pending::<SurfaceAttributes>()
|
||||
.buffer
|
||||
.as_ref()
|
||||
.and_then(|assignment| match assignment {
|
||||
BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).cloned().ok(),
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
if let Some(dmabuf) = maybe_dmabuf {
|
||||
if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
|
||||
if let Some(client) = surface.client() {
|
||||
let res =
|
||||
state
|
||||
.niri
|
||||
.event_loop
|
||||
.insert_source(source, move |_, _, state| {
|
||||
let display_handle = state.niri.display_handle.clone();
|
||||
state
|
||||
.client_compositor_state(&client)
|
||||
.blocker_cleared(state, &display_handle);
|
||||
Ok(())
|
||||
});
|
||||
if res.is_ok() {
|
||||
add_blocker(surface, blocker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
self.add_default_dmabuf_pre_commit_hook(surface);
|
||||
}
|
||||
|
||||
fn commit(&mut self, surface: &WlSurface) {
|
||||
let _span = tracy_client::span!("CompositorHandler::commit");
|
||||
trace!(surface = ?surface.id(), "commit");
|
||||
|
||||
on_commit_buffer_handler::<Self>(surface);
|
||||
self.backend.early_import(surface);
|
||||
@@ -104,25 +78,25 @@ impl CompositorHandler for State {
|
||||
if surface == &root_surface {
|
||||
// This is a root surface commit. It might have mapped a previously-unmapped toplevel.
|
||||
if let Entry::Occupied(entry) = self.niri.unmapped_windows.entry(surface.clone()) {
|
||||
let is_mapped =
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some())
|
||||
.unwrap_or_else(|| {
|
||||
error!("no renderer surface state even though we use commit handler");
|
||||
false
|
||||
});
|
||||
|
||||
if is_mapped {
|
||||
if is_mapped(surface) {
|
||||
// The toplevel got mapped.
|
||||
let Unmapped { window, state } = entry.remove();
|
||||
let Unmapped {
|
||||
window,
|
||||
state,
|
||||
activation_token_data,
|
||||
} = entry.remove();
|
||||
|
||||
window.on_commit();
|
||||
|
||||
let toplevel = window.toplevel().expect("no X11 support");
|
||||
|
||||
let (rules, width, is_full_width, output, workspace_name) =
|
||||
let (rules, width, height, is_full_width, output, workspace_id) =
|
||||
if let InitialConfigureState::Configured {
|
||||
rules,
|
||||
width,
|
||||
height,
|
||||
floating_width: _,
|
||||
floating_height: _,
|
||||
is_full_width,
|
||||
output,
|
||||
workspace_name,
|
||||
@@ -132,16 +106,49 @@ impl CompositorHandler for State {
|
||||
let output =
|
||||
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
|
||||
|
||||
// Chech that the workspace still exists.
|
||||
let workspace_name = workspace_name
|
||||
.filter(|n| self.niri.layout.find_workspace_by_name(n).is_some());
|
||||
// Check that the workspace still exists.
|
||||
let workspace_id = workspace_name
|
||||
.as_deref()
|
||||
.and_then(|n| self.niri.layout.find_workspace_by_name(n))
|
||||
.map(|(_, ws)| ws.id());
|
||||
|
||||
(rules, width, is_full_width, output, workspace_name)
|
||||
(rules, width, height, is_full_width, output, workspace_id)
|
||||
} else {
|
||||
error!("window map must happen after initial configure");
|
||||
(ResolvedWindowRules::empty(), None, false, None, None)
|
||||
(ResolvedWindowRules::empty(), None, None, false, None, None)
|
||||
};
|
||||
|
||||
// The GTK about dialog sets min/max size after the initial configure but
|
||||
// before mapping, so we need to compute open_floating at the last possible
|
||||
// moment, that is here.
|
||||
let is_floating = rules.compute_open_floating(toplevel);
|
||||
|
||||
// Figure out if we should activate the window.
|
||||
let activate = rules.open_focused.map(|focus| {
|
||||
if focus {
|
||||
ActivateWindow::Yes
|
||||
} else {
|
||||
ActivateWindow::No
|
||||
}
|
||||
});
|
||||
let activate = activate.unwrap_or_else(|| {
|
||||
// Check the token timestamp again in case the window took a while between
|
||||
// requesting activation and mapping.
|
||||
let token = activation_token_data.filter(|token| {
|
||||
token.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT
|
||||
});
|
||||
if token.is_some() {
|
||||
ActivateWindow::Yes
|
||||
} else {
|
||||
let config = self.niri.config.borrow();
|
||||
if config.debug.strict_new_window_focus_policy {
|
||||
ActivateWindow::No
|
||||
} else {
|
||||
ActivateWindow::Smart
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let parent = toplevel
|
||||
.parent()
|
||||
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||
@@ -152,42 +159,46 @@ impl CompositorHandler for State {
|
||||
// None. If the configured output is set, that means it was set explicitly
|
||||
// by a window rule or a fullscreen request.
|
||||
.filter(|(_, parent_output)| {
|
||||
output.is_none() || output.as_ref() == Some(*parent_output)
|
||||
parent_output.is_none()
|
||||
|| output.is_none()
|
||||
|| output.as_ref() == *parent_output
|
||||
})
|
||||
.map(|(mapped, _)| mapped.window.clone());
|
||||
|
||||
// The mapped pre-commit hook deals with dma-bufs on its own.
|
||||
self.remove_default_dmabuf_pre_commit_hook(toplevel.wl_surface());
|
||||
let hook = add_mapped_toplevel_pre_commit_hook(toplevel);
|
||||
let mapped = Mapped::new(window, rules, hook);
|
||||
let window = mapped.window.clone();
|
||||
|
||||
let output = if let Some(p) = parent {
|
||||
// Open dialogs immediately to the right of their parent window.
|
||||
self.niri
|
||||
.layout
|
||||
.add_window_right_of(&p, mapped, width, is_full_width)
|
||||
} else if let Some(workspace_name) = &workspace_name {
|
||||
self.niri.layout.add_window_to_named_workspace(
|
||||
workspace_name,
|
||||
mapped,
|
||||
width,
|
||||
is_full_width,
|
||||
)
|
||||
let target = if let Some(p) = &parent {
|
||||
// Open dialogs next to their parent window.
|
||||
AddWindowTarget::NextTo(p)
|
||||
} else if let Some(id) = workspace_id {
|
||||
AddWindowTarget::Workspace(id)
|
||||
} else if let Some(output) = &output {
|
||||
self.niri
|
||||
.layout
|
||||
.add_window_on_output(output, mapped, width, is_full_width);
|
||||
Some(output)
|
||||
AddWindowTarget::Output(output)
|
||||
} else {
|
||||
self.niri.layout.add_window(mapped, width, is_full_width)
|
||||
AddWindowTarget::Auto
|
||||
};
|
||||
let output = self.niri.layout.add_window(
|
||||
mapped,
|
||||
target,
|
||||
width,
|
||||
height,
|
||||
is_full_width,
|
||||
is_floating,
|
||||
activate,
|
||||
);
|
||||
|
||||
if let Some(output) = output.cloned() {
|
||||
self.niri.layout.start_open_animation_for_window(&window);
|
||||
|
||||
let new_active_window =
|
||||
self.niri.layout.active_window().map(|(m, _)| &m.window);
|
||||
if new_active_window == Some(&window) {
|
||||
let new_focus = self.niri.layout.focus().map(|m| &m.window);
|
||||
if new_focus == Some(&window) {
|
||||
// We activated the newly opened window.
|
||||
self.maybe_warp_cursor_to_focus();
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
}
|
||||
|
||||
self.niri.queue_redraw(&output);
|
||||
@@ -207,22 +218,21 @@ impl CompositorHandler for State {
|
||||
// This is a commit of a previously-mapped root or a non-toplevel root.
|
||||
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(surface) {
|
||||
let window = mapped.window.clone();
|
||||
let output = output.clone();
|
||||
let output = output.cloned();
|
||||
|
||||
let id = mapped.id();
|
||||
|
||||
// This is a commit of a previously-mapped toplevel.
|
||||
let is_mapped =
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some())
|
||||
.unwrap_or_else(|| {
|
||||
error!("no renderer surface state even though we use commit handler");
|
||||
false
|
||||
});
|
||||
let is_mapped = is_mapped(surface);
|
||||
|
||||
// Must start the close animation before window.on_commit().
|
||||
let transaction = Transaction::new();
|
||||
if !is_mapped {
|
||||
let blocker = transaction.blocker();
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
self.niri
|
||||
.layout
|
||||
.start_close_animation_for_window(renderer, &window);
|
||||
.start_close_animation_for_window(renderer, &window, blocker);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -232,10 +242,20 @@ impl CompositorHandler for State {
|
||||
// The toplevel got unmapped.
|
||||
//
|
||||
// Test client: wleird-unmap.
|
||||
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
|
||||
let active_window = self.niri.layout.focus().map(|m| &m.window);
|
||||
let was_active = active_window == Some(&window);
|
||||
|
||||
self.niri.layout.remove_window(&window);
|
||||
self.niri
|
||||
.stop_casts_for_target(CastTarget::Window { id: id.get() });
|
||||
|
||||
self.niri.layout.remove_window(&window, transaction.clone());
|
||||
self.add_default_dmabuf_pre_commit_hook(surface);
|
||||
|
||||
// If this is the only instance, then this transaction will complete
|
||||
// immediately, so no need to set the timer.
|
||||
if !transaction.is_last() {
|
||||
transaction.register_deadline_timer(&self.niri.event_loop);
|
||||
}
|
||||
|
||||
if was_active {
|
||||
self.maybe_warp_cursor_to_focus();
|
||||
@@ -246,18 +266,27 @@ impl CompositorHandler for State {
|
||||
let unmapped = Unmapped::new(window);
|
||||
self.niri.unmapped_windows.insert(surface.clone(), unmapped);
|
||||
|
||||
self.niri.queue_redraw(&output);
|
||||
if let Some(output) = output {
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let serial = with_states(surface, |states| {
|
||||
let (serial, buffer_delta) = with_states(surface, |states| {
|
||||
let buffer_delta = states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.buffer_delta
|
||||
.take();
|
||||
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
role.configure_serial
|
||||
(role.configure_serial, buffer_delta)
|
||||
});
|
||||
if serial.is_none() {
|
||||
error!("commit on a mapped surface without a configured serial");
|
||||
@@ -266,10 +295,25 @@ impl CompositorHandler for State {
|
||||
// The toplevel remains mapped.
|
||||
self.niri.layout.update_window(&window, serial);
|
||||
|
||||
// Popup placement depends on window size which might have changed.
|
||||
self.update_reactive_popups(&window, &output);
|
||||
// Move the toplevel according to the attach offset.
|
||||
if let Some(delta) = buffer_delta {
|
||||
if delta.x != 0 || delta.y != 0 {
|
||||
let (x, y) = delta.to_f64().into();
|
||||
self.niri.layout.move_floating_window(
|
||||
Some(&window),
|
||||
PositionChange::AdjustFixed(x),
|
||||
PositionChange::AdjustFixed(y),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.queue_redraw(&output);
|
||||
// Popup placement depends on window size which might have changed.
|
||||
self.update_reactive_popups(&window);
|
||||
|
||||
if let Some(output) = output {
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -280,10 +324,12 @@ impl CompositorHandler for State {
|
||||
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
|
||||
if let Some((mapped, output)) = root_window_output {
|
||||
let window = mapped.window.clone();
|
||||
let output = output.clone();
|
||||
let output = output.cloned();
|
||||
window.on_commit();
|
||||
self.niri.layout.update_window(&window, None);
|
||||
self.niri.queue_redraw(&output);
|
||||
if let Some(output) = output {
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -293,35 +339,88 @@ impl CompositorHandler for State {
|
||||
if let Some(output) = self.output_for_popup(&popup) {
|
||||
self.niri.queue_redraw(&output.clone());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// This might be a layer-shell surface.
|
||||
self.layer_shell_handle_commit(surface);
|
||||
if self.layer_shell_handle_commit(surface) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This might be a cursor surface.
|
||||
if matches!(&self.niri.cursor_manager.cursor_image(), CursorImageStatus::Surface(s) if s == surface)
|
||||
{
|
||||
if matches!(
|
||||
&self.niri.cursor_manager.cursor_image(),
|
||||
CursorImageStatus::Surface(s) if s == &root_surface
|
||||
) {
|
||||
// In case the cursor surface has been committed handle the role specific
|
||||
// buffer offset by applying the offset on the cursor image hotspot
|
||||
if surface == &root_surface {
|
||||
with_states(surface, |states| {
|
||||
let cursor_image_attributes = states.data_map.get::<CursorImageSurfaceData>();
|
||||
|
||||
if let Some(mut cursor_image_attributes) =
|
||||
cursor_image_attributes.map(|attrs| attrs.lock().unwrap())
|
||||
{
|
||||
let buffer_delta = states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.buffer_delta
|
||||
.take();
|
||||
if let Some(buffer_delta) = buffer_delta {
|
||||
cursor_image_attributes.hotspot -= buffer_delta;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: granular redraws for cursors.
|
||||
self.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
|
||||
// This might be a DnD icon surface.
|
||||
if self.niri.dnd_icon.as_ref() == Some(surface) {
|
||||
if matches!(&self.niri.dnd_icon, Some(icon) if icon.surface == root_surface) {
|
||||
let dnd_icon = self.niri.dnd_icon.as_mut().unwrap();
|
||||
|
||||
// In case the dnd surface has been committed handle the role specific
|
||||
// buffer offset by applying the offset on the dnd icon offset
|
||||
if surface == &dnd_icon.surface {
|
||||
with_states(&dnd_icon.surface, |states| {
|
||||
let buffer_delta = states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.buffer_delta
|
||||
.take()
|
||||
.unwrap_or_default();
|
||||
dnd_icon.offset += buffer_delta;
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: granular redraws for cursors.
|
||||
self.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
|
||||
// This might be a lock surface.
|
||||
if self.niri.is_locked() {
|
||||
for (output, state) in &self.niri.output_state {
|
||||
if let Some(lock_surface) = &state.lock_surface {
|
||||
if lock_surface.wl_surface() == &root_surface {
|
||||
for (output, state) in &self.niri.output_state {
|
||||
if let Some(lock_surface) = &state.lock_surface {
|
||||
if lock_surface.wl_surface() == &root_surface {
|
||||
if matches!(self.niri.lock_state, LockState::WaitingForSurfaces { .. }) {
|
||||
self.niri.maybe_continue_to_locking();
|
||||
} else {
|
||||
self.niri.queue_redraw(&output.clone());
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This message can trigger for lock surfaces that had a commit right after we unlocked
|
||||
// the session, but that's ok, we don't need to handle them.
|
||||
trace!("commit on an unrecognized surface: {surface:?}, root: {root_surface:?}");
|
||||
}
|
||||
|
||||
fn destroyed(&mut self, surface: &WlSurface) {
|
||||
@@ -346,6 +445,8 @@ impl CompositorHandler for State {
|
||||
self.niri
|
||||
.root_surface
|
||||
.retain(|k, v| k != surface && v != surface);
|
||||
|
||||
self.niri.dmabuf_pre_commit_hook.remove(surface);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,3 +462,57 @@ impl ShmHandler for State {
|
||||
|
||||
delegate_compositor!(State);
|
||||
delegate_shm!(State);
|
||||
|
||||
impl State {
|
||||
pub fn add_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
|
||||
let hook = add_pre_commit_hook::<Self, _>(surface, move |state, _dh, surface| {
|
||||
let maybe_dmabuf = with_states(surface, |surface_data| {
|
||||
surface_data
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.pending()
|
||||
.buffer
|
||||
.as_ref()
|
||||
.and_then(|assignment| match assignment {
|
||||
BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).cloned().ok(),
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
if let Some(dmabuf) = maybe_dmabuf {
|
||||
if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
|
||||
if let Some(client) = surface.client() {
|
||||
let res =
|
||||
state
|
||||
.niri
|
||||
.event_loop
|
||||
.insert_source(source, move |_, _, state| {
|
||||
let display_handle = state.niri.display_handle.clone();
|
||||
state
|
||||
.client_compositor_state(&client)
|
||||
.blocker_cleared(state, &display_handle);
|
||||
Ok(())
|
||||
});
|
||||
if res.is_ok() {
|
||||
add_blocker(surface, blocker);
|
||||
trace!("added default dmabuf blocker");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let s = surface.clone();
|
||||
if let Some(prev) = self.niri.dmabuf_pre_commit_hook.insert(s, hook) {
|
||||
error!("tried to add dmabuf pre-commit hook when there was already one");
|
||||
remove_pre_commit_hook(surface, prev);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
|
||||
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
|
||||
remove_pre_commit_hook(surface, hook);
|
||||
} else {
|
||||
error!("tried to remove dmabuf pre-commit hook but there was none");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+140
-36
@@ -3,14 +3,16 @@ use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurf
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::compositor::{send_surface_state, with_states};
|
||||
use smithay::wayland::compositor::{get_parent, with_states};
|
||||
use smithay::wayland::shell::wlr_layer::{
|
||||
Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
|
||||
self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
|
||||
WlrLayerShellState,
|
||||
};
|
||||
use smithay::wayland::shell::xdg::PopupSurface;
|
||||
|
||||
use crate::layer::{MappedLayer, ResolvedLayerRules};
|
||||
use crate::niri::State;
|
||||
use crate::utils::{is_mapped, output_size, send_scale_transform};
|
||||
|
||||
impl WlrLayerShellHandler for State {
|
||||
fn shell_state(&mut self) -> &mut WlrLayerShellState {
|
||||
@@ -24,17 +26,30 @@ impl WlrLayerShellHandler for State {
|
||||
_layer: Layer,
|
||||
namespace: String,
|
||||
) {
|
||||
let output = wl_output
|
||||
.as_ref()
|
||||
.and_then(Output::from_resource)
|
||||
.or_else(|| self.niri.layout.active_output().cloned())
|
||||
.unwrap();
|
||||
let output = if let Some(wl_output) = &wl_output {
|
||||
Output::from_resource(wl_output)
|
||||
} else {
|
||||
self.niri.layout.active_output().cloned()
|
||||
};
|
||||
let Some(output) = output else {
|
||||
warn!("no output for new layer surface, closing");
|
||||
surface.send_close();
|
||||
return;
|
||||
};
|
||||
|
||||
let wl_surface = surface.wl_surface().clone();
|
||||
let is_new = self.niri.unmapped_layer_surfaces.insert(wl_surface);
|
||||
assert!(is_new);
|
||||
|
||||
let mut map = layer_map_for_output(&output);
|
||||
map.map_layer(&LayerSurface::new(surface, namespace))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn layer_destroyed(&mut self, surface: WlrLayerSurface) {
|
||||
let wl_surface = surface.wl_surface();
|
||||
self.niri.unmapped_layer_surfaces.remove(wl_surface);
|
||||
|
||||
let output = if let Some((output, mut map, layer)) =
|
||||
self.niri.layout.outputs().find_map(|o| {
|
||||
let map = layer_map_for_output(o);
|
||||
@@ -45,6 +60,7 @@ impl WlrLayerShellHandler for State {
|
||||
layer.map(|layer| (o.clone(), map, layer))
|
||||
}) {
|
||||
map.unmap_layer(&layer);
|
||||
self.niri.mapped_layer_surfaces.remove(&layer);
|
||||
Some(output)
|
||||
} else {
|
||||
None
|
||||
@@ -61,52 +77,140 @@ impl WlrLayerShellHandler for State {
|
||||
delegate_layer_shell!(State);
|
||||
|
||||
impl State {
|
||||
pub fn layer_shell_handle_commit(&mut self, surface: &WlSurface) {
|
||||
let Some(output) = self
|
||||
pub fn layer_shell_handle_commit(&mut self, surface: &WlSurface) -> bool {
|
||||
let mut root_surface = surface.clone();
|
||||
while let Some(parent) = get_parent(&root_surface) {
|
||||
root_surface = parent;
|
||||
}
|
||||
|
||||
let output = self
|
||||
.niri
|
||||
.layout
|
||||
.outputs()
|
||||
.find(|o| {
|
||||
let map = layer_map_for_output(o);
|
||||
map.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
|
||||
map.layer_for_surface(&root_surface, WindowSurfaceType::TOPLEVEL)
|
||||
.is_some()
|
||||
})
|
||||
.cloned()
|
||||
else {
|
||||
return;
|
||||
.cloned();
|
||||
let Some(output) = output else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let initial_configure_sent = with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<LayerSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.initial_configure_sent
|
||||
});
|
||||
if surface == &root_surface {
|
||||
let initial_configure_sent = with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<LayerSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.initial_configure_sent
|
||||
});
|
||||
|
||||
let mut map = layer_map_for_output(&output);
|
||||
let mut map = layer_map_for_output(&output);
|
||||
|
||||
// Arrange the layers before sending the initial configure to respect any size the
|
||||
// client may have sent.
|
||||
map.arrange();
|
||||
|
||||
// arrange the layers before sending the initial configure
|
||||
// to respect any size the client may have sent
|
||||
map.arrange();
|
||||
// send the initial configure if relevant
|
||||
if !initial_configure_sent {
|
||||
let layer = map
|
||||
.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
|
||||
.unwrap();
|
||||
|
||||
let scale = output.current_scale().integer_scale();
|
||||
let transform = output.current_transform();
|
||||
with_states(surface, |data| {
|
||||
send_surface_state(surface, data, scale, transform);
|
||||
});
|
||||
if initial_configure_sent {
|
||||
if is_mapped(surface) {
|
||||
let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface);
|
||||
|
||||
layer.layer_surface().send_configure();
|
||||
// Resolve rules for newly mapped layer surfaces.
|
||||
if was_unmapped {
|
||||
let config = self.niri.config.borrow();
|
||||
|
||||
let rules = &config.layer_rules;
|
||||
let rules =
|
||||
ResolvedLayerRules::compute(rules, layer, self.niri.is_at_startup);
|
||||
|
||||
let output_size = output_size(&output);
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
|
||||
let mapped = MappedLayer::new(
|
||||
layer.clone(),
|
||||
rules,
|
||||
output_size,
|
||||
scale,
|
||||
self.niri.clock.clone(),
|
||||
&config,
|
||||
);
|
||||
|
||||
let prev = self
|
||||
.niri
|
||||
.mapped_layer_surfaces
|
||||
.insert(layer.clone(), mapped);
|
||||
if prev.is_some() {
|
||||
error!("MappedLayer was present for an unmapped surface");
|
||||
}
|
||||
}
|
||||
|
||||
// Give focus to newly mapped on-demand surfaces. Some launchers like
|
||||
// lxqt-runner rely on this behavior. While this behavior doesn't make much
|
||||
// sense for other clients like panels, the consensus seems to be that it's not
|
||||
// a big deal since panels generally only open once at the start of the
|
||||
// session.
|
||||
//
|
||||
// Note that:
|
||||
// 1) Exclusive layer surfaces already get focus automatically in
|
||||
// update_keyboard_focus().
|
||||
// 2) Same-layer exclusive layer surfaces are already preferred to on-demand
|
||||
// surfaces in update_keyboard_focus(), so we don't need to check for that
|
||||
// here.
|
||||
//
|
||||
// https://github.com/YaLTeR/niri/issues/641
|
||||
let on_demand = layer.cached_state().keyboard_interactivity
|
||||
== wlr_layer::KeyboardInteractivity::OnDemand;
|
||||
if was_unmapped && on_demand {
|
||||
// I guess it'd make sense to check that no higher-layer on-demand surface
|
||||
// has focus, but Smithay's Layer doesn't implement Ord so this would be a
|
||||
// little annoying.
|
||||
self.niri.layer_shell_on_demand_focus = Some(layer.clone());
|
||||
}
|
||||
} else {
|
||||
let was_mapped = self.niri.mapped_layer_surfaces.remove(layer).is_some();
|
||||
self.niri.unmapped_layer_surfaces.insert(surface.clone());
|
||||
|
||||
// After layer surface unmaps it has to perform the initial commit-configure
|
||||
// sequence again. This is a workaround until Smithay properly resets
|
||||
// initial_configure_sent upon the surface unmapping itself as it does for
|
||||
// toplevels.
|
||||
if was_mapped {
|
||||
with_states(surface, |states| {
|
||||
let mut data = states
|
||||
.data_map
|
||||
.get::<LayerSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
data.initial_configure_sent = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let scale = output.current_scale();
|
||||
let transform = output.current_transform();
|
||||
with_states(surface, |data| {
|
||||
send_scale_transform(surface, data, scale, transform);
|
||||
});
|
||||
|
||||
layer.layer_surface().send_configure();
|
||||
}
|
||||
drop(map);
|
||||
|
||||
// This will call queue_redraw() inside.
|
||||
self.niri.output_resized(&output);
|
||||
} else {
|
||||
// This is an unsync layer-shell subsurface.
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
drop(map);
|
||||
|
||||
self.niri.output_resized(&output);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
+358
-56
@@ -7,31 +7,39 @@ use std::io::Write;
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::drm::DrmNode;
|
||||
use smithay::backend::input::TabletToolDescriptor;
|
||||
use smithay::backend::input::{InputEvent, TabletToolDescriptor};
|
||||
use smithay::desktop::{PopupKind, PopupManager};
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
|
||||
use smithay::input::pointer::{
|
||||
CursorIcon, CursorImageStatus, CursorImageSurfaceData, PointerHandle,
|
||||
};
|
||||
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::rustix::fs::{fcntl_setfl, OFlags};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
|
||||
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::utils::{Logical, Rectangle, Size};
|
||||
use smithay::wayland::compositor::{send_surface_state, with_states};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
use smithay::wayland::compositor::{get_parent, with_states};
|
||||
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
||||
use smithay::wayland::drm_lease::{
|
||||
DrmLease, DrmLeaseBuilder, DrmLeaseHandler, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
|
||||
};
|
||||
use smithay::wayland::fractional_scale::FractionalScaleHandler;
|
||||
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
|
||||
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
|
||||
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
|
||||
use smithay::wayland::keyboard_shortcuts_inhibit::{
|
||||
KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor,
|
||||
};
|
||||
use smithay::wayland::output::OutputHandler;
|
||||
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
|
||||
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler};
|
||||
use smithay::wayland::security_context::{
|
||||
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
|
||||
};
|
||||
@@ -39,32 +47,56 @@ use smithay::wayland::selection::data_device::{
|
||||
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
|
||||
ServerDndGrabHandler,
|
||||
};
|
||||
use smithay::wayland::selection::ext_data_control::{
|
||||
DataControlHandler as ExtDataControlHandler, DataControlState as ExtDataControlState,
|
||||
};
|
||||
use smithay::wayland::selection::primary_selection::{
|
||||
set_primary_focus, PrimarySelectionHandler, PrimarySelectionState,
|
||||
};
|
||||
use smithay::wayland::selection::wlr_data_control::{DataControlHandler, DataControlState};
|
||||
use smithay::wayland::selection::wlr_data_control::{
|
||||
DataControlHandler as WlrDataControlHandler, DataControlState as WlrDataControlState,
|
||||
};
|
||||
use smithay::wayland::selection::{SelectionHandler, SelectionTarget};
|
||||
use smithay::wayland::session_lock::{
|
||||
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
|
||||
};
|
||||
use smithay::wayland::tablet_manager::TabletSeatHandler;
|
||||
use smithay::wayland::xdg_activation::{
|
||||
XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,
|
||||
};
|
||||
use smithay::{
|
||||
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
|
||||
delegate_drm_lease, delegate_idle_inhibit, delegate_idle_notify, delegate_input_method_manager,
|
||||
delegate_output, delegate_pointer_constraints, delegate_pointer_gestures,
|
||||
delegate_presentation, delegate_primary_selection, delegate_relative_pointer, delegate_seat,
|
||||
delegate_security_context, delegate_session_lock, delegate_tablet_manager,
|
||||
delegate_text_input_manager, delegate_viewporter, delegate_virtual_keyboard_manager,
|
||||
delegate_drm_lease, delegate_ext_data_control, delegate_fractional_scale,
|
||||
delegate_idle_inhibit, delegate_idle_notify, delegate_input_method_manager,
|
||||
delegate_keyboard_shortcuts_inhibit, delegate_output, delegate_pointer_constraints,
|
||||
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
|
||||
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
|
||||
delegate_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager,
|
||||
delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation,
|
||||
};
|
||||
|
||||
use crate::niri::{ClientState, State};
|
||||
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
|
||||
use crate::layout::ActivateWindow;
|
||||
use crate::niri::{DndIcon, NewClient, State};
|
||||
use crate::protocols::foreign_toplevel::{
|
||||
self, ForeignToplevelHandler, ForeignToplevelManagerState,
|
||||
};
|
||||
use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerState};
|
||||
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler};
|
||||
use crate::utils::output_size;
|
||||
use crate::{delegate_foreign_toplevel, delegate_gamma_control, delegate_screencopy};
|
||||
use crate::protocols::mutter_x11_interop::MutterX11InteropHandler;
|
||||
use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState};
|
||||
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState};
|
||||
use crate::protocols::virtual_pointer::{
|
||||
VirtualPointerAxisEvent, VirtualPointerButtonEvent, VirtualPointerHandler,
|
||||
VirtualPointerInputBackend, VirtualPointerManagerState, VirtualPointerMotionAbsoluteEvent,
|
||||
VirtualPointerMotionEvent,
|
||||
};
|
||||
use crate::utils::{output_size, send_scale_transform, with_toplevel_role};
|
||||
use crate::{
|
||||
delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop,
|
||||
delegate_output_management, delegate_screencopy, delegate_virtual_pointer,
|
||||
};
|
||||
|
||||
pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
impl SeatHandler for State {
|
||||
type KeyboardFocus = WlSurface;
|
||||
@@ -123,11 +155,66 @@ impl TabletSeatHandler for State {
|
||||
delegate_tablet_manager!(State);
|
||||
|
||||
impl PointerConstraintsHandler for State {
|
||||
fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle<Self>) {
|
||||
self.niri.maybe_activate_pointer_constraint(
|
||||
pointer.current_location(),
|
||||
&self.niri.pointer_focus,
|
||||
);
|
||||
fn new_constraint(&mut self, _surface: &WlSurface, _pointer: &PointerHandle<Self>) {
|
||||
// Pointer constraints track pointer focus internally, so make sure it's up to date before
|
||||
// activating a new one.
|
||||
self.refresh_pointer_contents();
|
||||
|
||||
self.niri.maybe_activate_pointer_constraint();
|
||||
}
|
||||
|
||||
fn cursor_position_hint(
|
||||
&mut self,
|
||||
surface: &WlSurface,
|
||||
pointer: &PointerHandle<Self>,
|
||||
location: Point<f64, Logical>,
|
||||
) {
|
||||
let is_constraint_active = with_pointer_constraint(surface, pointer, |constraint| {
|
||||
constraint.is_some_and(|c| c.is_active())
|
||||
});
|
||||
|
||||
if !is_constraint_active {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: this is surface under pointer, not pointer focus. So if you start, say, a
|
||||
// middle-drag in Blender, then touchpad-swipe the window away, the surface under pointer
|
||||
// will change, even though the real pointer focus remains on the Blender surface due to
|
||||
// the click grab.
|
||||
//
|
||||
// Ideally we would just use the constraint surface, but we need its origin. So this is
|
||||
// more of a hack because pointer contents has the surface origin available.
|
||||
//
|
||||
// FIXME: use the constraint surface somehow, don't use pointer contents.
|
||||
let Some((ref surface_under_pointer, origin)) = self.niri.pointer_contents.surface else {
|
||||
return;
|
||||
};
|
||||
|
||||
if surface_under_pointer != surface {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut root = surface.clone();
|
||||
while let Some(parent) = get_parent(&root) {
|
||||
root = parent;
|
||||
}
|
||||
|
||||
let target = self
|
||||
.niri
|
||||
.output_for_root(&root)
|
||||
.and_then(|output| self.niri.global_space.output_geometry(output))
|
||||
.map_or(origin + location, |mut output_geometry| {
|
||||
// i32 sizes are exclusive, but f64 sizes are inclusive.
|
||||
output_geometry.size -= (1, 1).into();
|
||||
(origin + location).constrain(output_geometry.to_f64())
|
||||
});
|
||||
pointer.set_location(target);
|
||||
|
||||
// Redraw to update the cursor position if it's visible.
|
||||
if self.niri.pointer_visibility.is_visible() {
|
||||
// FIXME: redraw only outputs overlapping the cursor.
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate_pointer_constraints!(State);
|
||||
@@ -136,11 +223,11 @@ impl InputMethodHandler for State {
|
||||
fn new_popup(&mut self, surface: PopupSurface) {
|
||||
let popup = PopupKind::InputMethod(surface);
|
||||
if let Some(output) = self.output_for_popup(&popup) {
|
||||
let scale = output.current_scale().integer_scale();
|
||||
let scale = output.current_scale();
|
||||
let transform = output.current_transform();
|
||||
let wl_surface = popup.wl_surface();
|
||||
with_states(wl_surface, |data| {
|
||||
send_surface_state(wl_surface, data, scale, transform);
|
||||
send_scale_transform(wl_surface, data, scale, transform);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -171,7 +258,28 @@ impl InputMethodHandler for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyboardShortcutsInhibitHandler for State {
|
||||
fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState {
|
||||
&mut self.niri.keyboard_shortcuts_inhibit_state
|
||||
}
|
||||
|
||||
fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
|
||||
// FIXME: show a confirmation dialog with a "remember for this application" kind of toggle.
|
||||
inhibitor.activate();
|
||||
self.niri
|
||||
.keyboard_shortcuts_inhibiting_surfaces
|
||||
.insert(inhibitor.wl_surface().clone(), inhibitor);
|
||||
}
|
||||
|
||||
fn inhibitor_destroyed(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
|
||||
self.niri
|
||||
.keyboard_shortcuts_inhibiting_surfaces
|
||||
.remove(&inhibitor.wl_surface().clone());
|
||||
}
|
||||
}
|
||||
|
||||
delegate_input_method_manager!(State);
|
||||
delegate_keyboard_shortcuts_inhibit!(State);
|
||||
delegate_virtual_keyboard_manager!(State);
|
||||
|
||||
impl SelectionHandler for State {
|
||||
@@ -213,12 +321,63 @@ impl ClientDndGrabHandler for State {
|
||||
icon: Option<WlSurface>,
|
||||
_seat: Seat<Self>,
|
||||
) {
|
||||
self.niri.dnd_icon = icon;
|
||||
let offset = if let CursorImageStatus::Surface(ref surface) =
|
||||
self.niri.cursor_manager.cursor_image()
|
||||
{
|
||||
with_states(surface, |states| {
|
||||
let hotspot = states
|
||||
.data_map
|
||||
.get::<CursorImageSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.hotspot;
|
||||
Point::from((-hotspot.x, -hotspot.y))
|
||||
})
|
||||
} else {
|
||||
(0, 0).into()
|
||||
};
|
||||
self.niri.dnd_icon = icon.map(|surface| DndIcon { surface, offset });
|
||||
// FIXME: more granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
|
||||
fn dropped(&mut self, _seat: Seat<Self>) {
|
||||
fn dropped(&mut self, target: Option<WlSurface>, validated: bool, _seat: Seat<Self>) {
|
||||
trace!("client dropped, target: {target:?}, validated: {validated}");
|
||||
|
||||
// End DnD before activating a specific window below so that it takes precedence.
|
||||
self.niri.layout.dnd_end();
|
||||
|
||||
// Activate the target output, since that's how Firefox drag-tab-into-new-window works for
|
||||
// example. On successful drop, additionally activate the target window.
|
||||
let mut activate_output = true;
|
||||
if let Some(target) = validated.then_some(target).flatten() {
|
||||
let root = self.niri.find_root_shell_surface(&target);
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&root) {
|
||||
let window = mapped.window.clone();
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
activate_output = false;
|
||||
}
|
||||
}
|
||||
|
||||
if activate_output {
|
||||
// Find the output from cursor coordinates.
|
||||
//
|
||||
// FIXME: uhhh, we can't actually properly tell if the DnD comes from pointer or touch,
|
||||
// and if it comes from touch, then what the coordinates are. Need to pass more
|
||||
// parameters from Smithay I guess.
|
||||
//
|
||||
// Assume that hidden pointer means touch DnD.
|
||||
if self.niri.pointer_visibility.is_visible() {
|
||||
// We can't even get the current pointer location because it's locked (we're deep
|
||||
// in the grab call stack here). So use the last known one.
|
||||
if let Some(output) = &self.niri.pointer_contents.output {
|
||||
self.niri.layout.focus_output(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.dnd_icon = None;
|
||||
// FIXME: more granular
|
||||
self.niri.queue_redraw_all();
|
||||
@@ -236,14 +395,22 @@ impl PrimarySelectionHandler for State {
|
||||
}
|
||||
delegate_primary_selection!(State);
|
||||
|
||||
impl DataControlHandler for State {
|
||||
fn data_control_state(&self) -> &DataControlState {
|
||||
&self.niri.data_control_state
|
||||
impl WlrDataControlHandler for State {
|
||||
fn data_control_state(&self) -> &WlrDataControlState {
|
||||
&self.niri.wlr_data_control_state
|
||||
}
|
||||
}
|
||||
|
||||
delegate_data_control!(State);
|
||||
|
||||
impl ExtDataControlHandler for State {
|
||||
fn data_control_state(&self) -> &ExtDataControlState {
|
||||
&self.niri.ext_data_control_state
|
||||
}
|
||||
}
|
||||
|
||||
delegate_ext_data_control!(State);
|
||||
|
||||
impl OutputHandler for State {
|
||||
fn output_bound(&mut self, output: Output, wl_output: WlOutput) {
|
||||
foreign_toplevel::on_output_bound(self, &output, &wl_output);
|
||||
@@ -284,11 +451,13 @@ impl SessionLockHandler for State {
|
||||
|
||||
fn unlock(&mut self) {
|
||||
self.niri.unlock();
|
||||
self.niri.activate_monitors(&mut self.backend);
|
||||
self.niri.notify_activity();
|
||||
}
|
||||
|
||||
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
|
||||
let Some(output) = Output::from_resource(&output) else {
|
||||
error!("no Output matching WlOutput");
|
||||
warn!("no Output matching WlOutput");
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -303,11 +472,11 @@ pub fn configure_lock_surface(surface: &LockSurface, output: &Output) {
|
||||
let size = output_size(output);
|
||||
states.size = Some(Size::from((size.w as u32, size.h as u32)));
|
||||
});
|
||||
let scale = output.current_scale().integer_scale();
|
||||
let scale = output.current_scale();
|
||||
let transform = output.current_transform();
|
||||
let wl_surface = surface.wl_surface();
|
||||
with_states(wl_surface, |data| {
|
||||
send_surface_state(wl_surface, data, scale, transform);
|
||||
send_scale_transform(wl_surface, data, scale, transform);
|
||||
});
|
||||
surface.send_configure();
|
||||
}
|
||||
@@ -317,18 +486,12 @@ impl SecurityContextHandler for State {
|
||||
self.niri
|
||||
.event_loop
|
||||
.insert_source(source, move |client, _, state| {
|
||||
let config = state.niri.config.borrow();
|
||||
let data = Arc::new(ClientState {
|
||||
compositor_state: Default::default(),
|
||||
can_view_decoration_globals: config.prefer_no_csd,
|
||||
trace!("inserting a new restricted client, context={context:?}");
|
||||
state.niri.insert_client(NewClient {
|
||||
client,
|
||||
restricted: true,
|
||||
credentials_unknown: false,
|
||||
});
|
||||
|
||||
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
|
||||
warn!("error inserting client: {err}");
|
||||
} else {
|
||||
trace!("inserted a new restricted client, context={context:?}");
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
@@ -362,6 +525,7 @@ impl ForeignToplevelHandler for State {
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
|
||||
let window = mapped.window.clone();
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
@@ -375,22 +539,25 @@ impl ForeignToplevelHandler for State {
|
||||
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>) {
|
||||
if let Some((mapped, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
|
||||
{
|
||||
if !mapped
|
||||
.toplevel()
|
||||
.current_state()
|
||||
.capabilities
|
||||
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
|
||||
{
|
||||
let has_fullscreen_cap = with_toplevel_role(mapped.toplevel(), |role| {
|
||||
role.current
|
||||
.capabilities
|
||||
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
|
||||
});
|
||||
if !has_fullscreen_cap {
|
||||
return;
|
||||
}
|
||||
|
||||
let window = mapped.window.clone();
|
||||
|
||||
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
|
||||
if &requested_output != current_output {
|
||||
self.niri
|
||||
.layout
|
||||
.move_window_to_output(&window, &requested_output);
|
||||
if Some(&requested_output) != current_output {
|
||||
self.niri.layout.move_to_output(
|
||||
Some(&window),
|
||||
&requested_output,
|
||||
None,
|
||||
ActivateWindow::Smart,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,17 +575,58 @@ impl ForeignToplevelHandler for State {
|
||||
delegate_foreign_toplevel!(State);
|
||||
|
||||
impl ScreencopyHandler for State {
|
||||
fn frame(&mut self, screencopy: Screencopy) {
|
||||
if let Err(err) = self
|
||||
.niri
|
||||
.render_for_screencopy(&mut self.backend, screencopy)
|
||||
{
|
||||
warn!("error rendering for screencopy: {err:?}");
|
||||
fn frame(&mut self, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy) {
|
||||
// If with_damage then push it onto the queue for redraw of the output,
|
||||
// otherwise render it immediately.
|
||||
if screencopy.with_damage() {
|
||||
let Some(queue) = self.niri.screencopy_state.get_queue_mut(manager) else {
|
||||
trace!("screencopy manager destroyed already");
|
||||
return;
|
||||
};
|
||||
queue.push(screencopy);
|
||||
} else {
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
if let Err(err) = self
|
||||
.niri
|
||||
.render_for_screencopy_without_damage(renderer, manager, screencopy)
|
||||
{
|
||||
warn!("error rendering for screencopy: {err:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn screencopy_state(&mut self) -> &mut ScreencopyManagerState {
|
||||
&mut self.niri.screencopy_state
|
||||
}
|
||||
}
|
||||
delegate_screencopy!(State);
|
||||
|
||||
impl VirtualPointerHandler for State {
|
||||
fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState {
|
||||
&mut self.niri.virtual_pointer_state
|
||||
}
|
||||
|
||||
fn on_virtual_pointer_motion(&mut self, event: VirtualPointerMotionEvent) {
|
||||
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerMotion { event });
|
||||
}
|
||||
|
||||
fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent) {
|
||||
self.process_input_event(
|
||||
InputEvent::<VirtualPointerInputBackend>::PointerMotionAbsolute { event },
|
||||
);
|
||||
}
|
||||
|
||||
fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent) {
|
||||
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerButton { event });
|
||||
}
|
||||
|
||||
fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent) {
|
||||
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerAxis { event });
|
||||
}
|
||||
}
|
||||
delegate_virtual_pointer!(State);
|
||||
|
||||
impl DrmLeaseHandler for State {
|
||||
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
|
||||
self.backend
|
||||
@@ -498,3 +706,97 @@ impl GammaControlHandler for State {
|
||||
}
|
||||
}
|
||||
delegate_gamma_control!(State);
|
||||
|
||||
struct UrgentOnlyMarker;
|
||||
|
||||
impl XdgActivationHandler for State {
|
||||
fn activation_state(&mut self) -> &mut XdgActivationState {
|
||||
&mut self.niri.activation_state
|
||||
}
|
||||
|
||||
fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool {
|
||||
// Tokens without a serial are urgency-only. This is not specified, but it seems to be the
|
||||
// common client behavior.
|
||||
//
|
||||
// See also: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/150
|
||||
let Some((serial, seat)) = data.serial else {
|
||||
data.user_data.insert_if_missing(|| UrgentOnlyMarker);
|
||||
return true;
|
||||
};
|
||||
let Some(seat) = Seat::<State>::from_resource(&seat) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Widely-used clients such as Discord and Telegram make new tokens (with invalid serials)
|
||||
// upon clicking on their tray icon or on their notification. This debug flag makes that
|
||||
// work.
|
||||
//
|
||||
// Clicking on a notification sends clients a perfectly valid activation token from the
|
||||
// notification daemon, but alas they ignore it. Maybe in the future the clients are fixed,
|
||||
// and we can remove this debug flag.
|
||||
let config = self.niri.config.borrow();
|
||||
if config.debug.honor_xdg_activation_with_invalid_serial {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check the serial against both a keyboard and a pointer, since layer-shell surfaces
|
||||
// with no keyboard interactivity won't have any keyboard focus.
|
||||
let kb_last_enter = seat.get_keyboard().unwrap().last_enter();
|
||||
if kb_last_enter.is_some_and(|last_enter| serial.is_no_older_than(&last_enter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let pointer_last_enter = seat.get_pointer().unwrap().last_enter();
|
||||
if pointer_last_enter.is_some_and(|last_enter| serial.is_no_older_than(&last_enter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn request_activation(
|
||||
&mut self,
|
||||
token: XdgActivationToken,
|
||||
token_data: XdgActivationTokenData,
|
||||
surface: WlSurface,
|
||||
) {
|
||||
if token_data.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT {
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(&surface) {
|
||||
let window = mapped.window.clone();
|
||||
if token_data.user_data.get::<UrgentOnlyMarker>().is_some() {
|
||||
mapped.set_urgent(true);
|
||||
self.niri.queue_redraw_all();
|
||||
} else {
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(&surface) {
|
||||
unmapped.activation_token_data = Some(token_data);
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.activation_state.remove_token(&token);
|
||||
}
|
||||
}
|
||||
delegate_xdg_activation!(State);
|
||||
|
||||
impl FractionalScaleHandler for State {}
|
||||
delegate_fractional_scale!(State);
|
||||
|
||||
impl OutputManagementHandler for State {
|
||||
fn output_management_state(&mut self) -> &mut OutputManagementManagerState {
|
||||
&mut self.niri.output_management_state
|
||||
}
|
||||
|
||||
fn apply_output_config(&mut self, config: niri_config::Outputs) {
|
||||
self.niri.config.borrow_mut().outputs = config;
|
||||
self.reload_output_config();
|
||||
}
|
||||
}
|
||||
delegate_output_management!(State);
|
||||
|
||||
impl MutterX11InteropHandler for State {}
|
||||
delegate_mutter_x11_interop!(State);
|
||||
|
||||
delegate_single_pixel_buffer!(State);
|
||||
|
||||
+519
-167
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
use ::input as libinput;
|
||||
use smithay::backend::input;
|
||||
use smithay::backend::winit::WinitVirtualDevice;
|
||||
use smithay::output::Output;
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::protocols::virtual_pointer::VirtualPointer;
|
||||
|
||||
pub trait NiriInputBackend: input::InputBackend<Device = Self::NiriDevice> {
|
||||
type NiriDevice: NiriInputDevice;
|
||||
}
|
||||
impl<T: input::InputBackend> NiriInputBackend for T
|
||||
where
|
||||
Self::Device: NiriInputDevice,
|
||||
{
|
||||
type NiriDevice = Self::Device;
|
||||
}
|
||||
|
||||
pub trait NiriInputDevice: input::Device {
|
||||
// FIXME: this should maybe be per-event, not per-device,
|
||||
// but it's not clear that this matters in practice?
|
||||
// it might be more obvious once we implement it for libinput
|
||||
fn output(&self, state: &State) -> Option<Output>;
|
||||
}
|
||||
|
||||
impl NiriInputDevice for libinput::Device {
|
||||
fn output(&self, _state: &State) -> Option<Output> {
|
||||
// FIXME: Allow specifying the output per-device?
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl NiriInputDevice for WinitVirtualDevice {
|
||||
fn output(&self, _state: &State) -> Option<Output> {
|
||||
// FIXME: we should be returning the single output that the winit backend creates,
|
||||
// but for now, that will cause issues because the output is normally upside down,
|
||||
// so we apply Transform::Flipped180 to it and that would also cause
|
||||
// the cursor position to be flipped, which is not what we want.
|
||||
//
|
||||
// instead, we just return None and rely on the fact that it has only one output.
|
||||
// doing so causes the cursor to be placed in *global* output coordinates,
|
||||
// which are not flipped, and happen to be what we want.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl NiriInputDevice for VirtualPointer {
|
||||
fn output(&self, _: &State) -> Option<Output> {
|
||||
self.output().cloned()
|
||||
}
|
||||
}
|
||||
+2610
-421
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,240 @@
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent,
|
||||
GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent,
|
||||
GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
|
||||
GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
|
||||
RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{IsAlive, Logical, Point};
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct MoveGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
window: Window,
|
||||
gesture: GestureState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum GestureState {
|
||||
Recognizing,
|
||||
Move,
|
||||
}
|
||||
|
||||
impl MoveGrab {
|
||||
pub fn new(
|
||||
start_data: PointerGrabStartData<State>,
|
||||
window: Window,
|
||||
use_threshold: bool,
|
||||
) -> Self {
|
||||
let gesture = if use_threshold {
|
||||
GestureState::Recognizing
|
||||
} else {
|
||||
GestureState::Move
|
||||
};
|
||||
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
window,
|
||||
gesture,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_move_end(&self.window);
|
||||
// FIXME: only redraw the window output.
|
||||
state.niri.queue_redraw_all();
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for MoveGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.motion(data, None, event);
|
||||
|
||||
if self.window.alive() {
|
||||
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
|
||||
let output = output.clone();
|
||||
let event_delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
|
||||
if self.gesture == GestureState::Recognizing {
|
||||
let c = event.location - self.start_data.location;
|
||||
|
||||
// Check if the gesture moved far enough to decide.
|
||||
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
||||
self.gesture = GestureState::Move;
|
||||
|
||||
data.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
|
||||
}
|
||||
}
|
||||
|
||||
if self.gesture != GestureState::Move {
|
||||
return;
|
||||
}
|
||||
|
||||
let ongoing = data.niri.layout.interactive_move_update(
|
||||
&self.window,
|
||||
event_delta,
|
||||
output,
|
||||
pos_within_output,
|
||||
);
|
||||
if ongoing {
|
||||
// FIXME: only redraw the previous and the new output.
|
||||
data.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The move is no longer ongoing.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.relative_motion(data, None, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
handle.button(data, event);
|
||||
|
||||
// When moving with the left button, right toggles floating, and vice versa.
|
||||
let toggle_floating_button = if self.start_data.button == 0x110 {
|
||||
0x111
|
||||
} else {
|
||||
0x110
|
||||
};
|
||||
if event.button == toggle_floating_button && event.state == ButtonState::Pressed {
|
||||
data.niri.layout.toggle_window_floating(Some(&self.window));
|
||||
}
|
||||
|
||||
if !handle.current_pressed().contains(&self.start_data.button) {
|
||||
// The button that initiated the grab was released.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
use niri_ipc::PickedColor;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{Logical, Physical, Point, Scale, Size, Transform};
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::render_helpers::{render_to_vec, RenderTarget};
|
||||
|
||||
pub struct PickColorGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
}
|
||||
|
||||
impl PickColorGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>) -> Self {
|
||||
Self { start_data }
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
if let Some(tx) = state.niri.pick_color.take() {
|
||||
let _ = tx.send_blocking(None);
|
||||
}
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
state.niri.queue_redraw_all();
|
||||
}
|
||||
|
||||
fn pick_color_at_point(location: Point<f64, Logical>, data: &mut State) -> Option<PickedColor> {
|
||||
let (output, pos_within_output) = data.niri.output_under(location)?;
|
||||
let output = output.clone();
|
||||
|
||||
data.backend
|
||||
.with_primary_renderer(|renderer| {
|
||||
data.niri.update_render_elements(Some(&output));
|
||||
|
||||
let scale = Scale::from(output.current_scale().fractional_scale());
|
||||
// FIXME: perhaps replace floor with round once we figure out the pointer behavior
|
||||
// at the bottom/right edges of the monitors.
|
||||
let pos = pos_within_output.to_physical_precise_floor(scale);
|
||||
let size = Size::<i32, Physical>::from((1, 1));
|
||||
|
||||
let elements = data.niri.render(
|
||||
renderer,
|
||||
&output,
|
||||
false,
|
||||
// This is an interactive operation so we can render without blocking out.
|
||||
RenderTarget::Output,
|
||||
);
|
||||
|
||||
let pixels = match render_to_vec(
|
||||
renderer,
|
||||
size,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
Fourcc::Abgr8888,
|
||||
elements.iter().rev().map(|elem| {
|
||||
let offset = pos.upscale(-1);
|
||||
RelocateRenderElement::from_element(elem, offset, Relocate::Relative)
|
||||
}),
|
||||
) {
|
||||
Ok(pixels) => pixels,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
if pixels.len() == 4 {
|
||||
let rgb = [
|
||||
f64::from(pixels[0]) / 255.0,
|
||||
f64::from(pixels[1]) / 255.0,
|
||||
f64::from(pixels[2]) / 255.0,
|
||||
];
|
||||
Some(PickedColor { rgb })
|
||||
} else {
|
||||
error!(
|
||||
"unexpected pixel data length: {} (expected 4)",
|
||||
pixels.len()
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for PickColorGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
handle.motion(data, None, event);
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
handle.relative_motion(data, None, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
if event.state != ButtonState::Pressed {
|
||||
return;
|
||||
}
|
||||
|
||||
// We're handling this press, don't send the release to the window.
|
||||
data.niri.suppressed_buttons.insert(event.button);
|
||||
|
||||
if let Some(tx) = data.niri.pick_color.take() {
|
||||
let color = Self::pick_color_at_point(handle.current_location(), data);
|
||||
let _ = tx.send_blocking(color);
|
||||
}
|
||||
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
@@ -10,74 +9,48 @@ use smithay::input::SeatHandler;
|
||||
use smithay::utils::{Logical, Point};
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::window::Mapped;
|
||||
|
||||
pub struct ViewOffsetGrab {
|
||||
pub struct PickWindowGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
}
|
||||
|
||||
impl ViewOffsetGrab {
|
||||
impl PickWindowGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>) -> Self {
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
}
|
||||
Self { start_data }
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
let res = state
|
||||
.niri
|
||||
.layout
|
||||
.view_offset_gesture_end(false, Some(false));
|
||||
if let Some(output) = res {
|
||||
state.niri.queue_redraw(&output);
|
||||
if let Some(tx) = state.niri.pick_window.take() {
|
||||
let _ = tx.send_blocking(None);
|
||||
}
|
||||
|
||||
state.niri.pointer_grab_ongoing = false;
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
// Redraw to update the cursor.
|
||||
state.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for ViewOffsetGrab {
|
||||
impl PointerGrab<State> for PickWindowGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<i32, Logical>)>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.motion(data, None, event);
|
||||
|
||||
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||
let delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
|
||||
let res = data
|
||||
.niri
|
||||
.layout
|
||||
.view_offset_gesture_update(-delta.x, timestamp, false);
|
||||
if let Some(output) = res {
|
||||
if let Some(output) = output {
|
||||
data.niri.queue_redraw(&output);
|
||||
}
|
||||
} else {
|
||||
// The resize is no longer ongoing.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<i32, Logical>)>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.relative_motion(data, None, event);
|
||||
}
|
||||
|
||||
@@ -87,12 +60,22 @@ impl PointerGrab<State> for ViewOffsetGrab {
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
handle.button(data, event);
|
||||
|
||||
if handle.current_pressed().is_empty() {
|
||||
// No more buttons are pressed, release the grab.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
if event.state != ButtonState::Pressed {
|
||||
return;
|
||||
}
|
||||
|
||||
// We're handling this press, don't send the release to the window.
|
||||
data.niri.suppressed_buttons.insert(event.button);
|
||||
|
||||
if let Some(tx) = data.niri.pick_window.take() {
|
||||
let _ = tx.send_blocking(
|
||||
data.niri
|
||||
.window_under(handle.current_location())
|
||||
.map(Mapped::id),
|
||||
);
|
||||
}
|
||||
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
|
||||
fn axis(
|
||||
@@ -22,7 +22,6 @@ impl ResizeGrab {
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_resize_end(&self.window);
|
||||
state.niri.pointer_grab_ongoing = false;
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
@@ -35,7 +34,7 @@ impl PointerGrab<State> for ResizeGrab {
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<i32, Logical>)>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
@@ -60,7 +59,7 @@ impl PointerGrab<State> for ResizeGrab {
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<i32, Logical>)>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
//! Swipe gesture from scroll events.
|
||||
//!
|
||||
//! Tracks when to begin, update, and end a swipe gesture from pointer axis events, also whether
|
||||
//! the gesture is vertical or horizontal. Necessary because libinput only provides touchpad swipe
|
||||
//! gesture events for 3+ fingers.
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScrollSwipeGesture {
|
||||
ongoing: bool,
|
||||
vertical: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Action {
|
||||
BeginUpdate,
|
||||
Update,
|
||||
End,
|
||||
}
|
||||
|
||||
impl ScrollSwipeGesture {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
ongoing: false,
|
||||
vertical: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, dx: f64, dy: f64) -> Action {
|
||||
if dx == 0. && dy == 0. {
|
||||
self.ongoing = false;
|
||||
Action::End
|
||||
} else if !self.ongoing {
|
||||
self.ongoing = true;
|
||||
self.vertical = dy != 0.;
|
||||
Action::BeginUpdate
|
||||
} else {
|
||||
Action::Update
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) -> bool {
|
||||
if self.ongoing {
|
||||
self.ongoing = false;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_vertical(&self) -> bool {
|
||||
self.vertical
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ScrollSwipeGesture {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn begin(self) -> bool {
|
||||
self == Action::BeginUpdate
|
||||
}
|
||||
|
||||
pub fn end(self) -> bool {
|
||||
self == Action::End
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Logical, Point};
|
||||
|
||||
use crate::layout::workspace::WorkspaceId;
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct SpatialMovementGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
output: Output,
|
||||
workspace_id: WorkspaceId,
|
||||
gesture: GestureState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum GestureState {
|
||||
Recognizing,
|
||||
ViewOffset,
|
||||
WorkspaceSwitch,
|
||||
}
|
||||
|
||||
impl SpatialMovementGrab {
|
||||
pub fn new(
|
||||
start_data: PointerGrabStartData<State>,
|
||||
output: Output,
|
||||
workspace_id: WorkspaceId,
|
||||
is_view_offset: bool,
|
||||
) -> Self {
|
||||
let gesture = if is_view_offset {
|
||||
GestureState::ViewOffset
|
||||
} else {
|
||||
GestureState::Recognizing
|
||||
};
|
||||
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
output,
|
||||
workspace_id,
|
||||
gesture,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
let layout = &mut state.niri.layout;
|
||||
let res = match self.gesture {
|
||||
GestureState::Recognizing => None,
|
||||
GestureState::ViewOffset => layout.view_offset_gesture_end(Some(false)),
|
||||
GestureState::WorkspaceSwitch => layout.workspace_switch_gesture_end(Some(false)),
|
||||
};
|
||||
|
||||
if let Some(output) = res {
|
||||
state.niri.queue_redraw(&output);
|
||||
}
|
||||
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for SpatialMovementGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.motion(data, None, event);
|
||||
|
||||
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||
let delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
|
||||
let layout = &mut data.niri.layout;
|
||||
let res = match self.gesture {
|
||||
GestureState::Recognizing => {
|
||||
let c = event.location - self.start_data.location;
|
||||
|
||||
// Check if the gesture moved far enough to decide. Threshold copied from GTK 4.
|
||||
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
||||
if c.x.abs() > c.y.abs() {
|
||||
self.gesture = GestureState::ViewOffset;
|
||||
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(self.workspace_id) {
|
||||
if ws.current_output() == Some(&self.output) {
|
||||
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
|
||||
layout.view_offset_gesture_update(-c.x, timestamp, false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
self.gesture = GestureState::WorkspaceSwitch;
|
||||
layout.workspace_switch_gesture_begin(&self.output, false);
|
||||
layout.workspace_switch_gesture_update(-c.y, timestamp, false)
|
||||
}
|
||||
} else {
|
||||
Some(None)
|
||||
}
|
||||
}
|
||||
GestureState::ViewOffset => {
|
||||
layout.view_offset_gesture_update(-delta.x, timestamp, false)
|
||||
}
|
||||
GestureState::WorkspaceSwitch => {
|
||||
layout.workspace_switch_gesture_update(-delta.y, timestamp, false)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(output) = res {
|
||||
if let Some(output) = output {
|
||||
data.niri.queue_redraw(&output);
|
||||
}
|
||||
} else {
|
||||
// The move is no longer ongoing.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.relative_motion(data, None, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
handle.button(data, event);
|
||||
|
||||
if handle.current_pressed().is_empty() {
|
||||
// No more buttons are pressed, release the grab.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::touch::{
|
||||
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
|
||||
TouchGrab, TouchInnerHandle, UpEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{IsAlive, Logical, Point, Serial};
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct TouchMoveGrab {
|
||||
start_data: TouchGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
window: Window,
|
||||
}
|
||||
|
||||
impl TouchMoveGrab {
|
||||
pub fn new(start_data: TouchGrabStartData<State>, window: Window) -> Self {
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
window,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_move_end(&self.window);
|
||||
// FIXME: only redraw the window output.
|
||||
state.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
impl TouchGrab<State> for TouchMoveGrab {
|
||||
fn down(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &DownEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.down(data, None, event, seq);
|
||||
}
|
||||
|
||||
fn up(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &UpEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.up(data, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.motion(data, None, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.window.alive() {
|
||||
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
|
||||
let output = output.clone();
|
||||
let event_delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
let ongoing = data.niri.layout.interactive_move_update(
|
||||
&self.window,
|
||||
event_delta,
|
||||
output,
|
||||
pos_within_output,
|
||||
);
|
||||
if ongoing {
|
||||
// FIXME: only redraw the previous and the new output.
|
||||
data.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The move is no longer ongoing.
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.frame(data, seq);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.cancel(data, seq);
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn shape(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &ShapeEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.shape(data, event, seq);
|
||||
}
|
||||
|
||||
fn orientation(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &OrientationEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.orientation(data, event, seq);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &TouchGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::touch::{
|
||||
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
|
||||
TouchGrab, TouchInnerHandle, UpEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{IsAlive, Logical, Point, Serial};
|
||||
|
||||
use crate::layout::workspace::{Workspace, WorkspaceId};
|
||||
use crate::niri::State;
|
||||
use crate::window::Mapped;
|
||||
|
||||
// When the touch is stationary for this much time, it becomes an interactive move.
|
||||
const INTERACTIVE_MOVE_THRESHOLD: Duration = Duration::from_millis(500);
|
||||
|
||||
pub struct TouchOverviewGrab {
|
||||
start_data: TouchGrabStartData<State>,
|
||||
start_timestamp: Duration,
|
||||
last_location: Point<f64, Logical>,
|
||||
output: Output,
|
||||
start_pos_within_output: Point<f64, Logical>,
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
workspace_matched_narrow: bool,
|
||||
window: Option<Window>,
|
||||
gesture: GestureState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum GestureState {
|
||||
Recognizing,
|
||||
ViewOffset,
|
||||
WorkspaceSwitch,
|
||||
InteractiveMove,
|
||||
}
|
||||
|
||||
impl TouchOverviewGrab {
|
||||
pub fn new(
|
||||
start_data: TouchGrabStartData<State>,
|
||||
start_timestamp: Duration,
|
||||
output: Output,
|
||||
start_pos_within_output: Point<f64, Logical>,
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
workspace_matched_narrow: bool,
|
||||
window: Option<Window>,
|
||||
) -> Self {
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_timestamp,
|
||||
start_data,
|
||||
output,
|
||||
start_pos_within_output,
|
||||
workspace_id,
|
||||
workspace_matched_narrow,
|
||||
window,
|
||||
gesture: GestureState::Recognizing,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
let layout = &mut state.niri.layout;
|
||||
match self.gesture {
|
||||
GestureState::Recognizing => {
|
||||
// Tap to activate.
|
||||
layout.focus_output(&self.output);
|
||||
|
||||
// Activate the workspace if necessary.
|
||||
if self.window.is_some() || self.workspace_matched_narrow {
|
||||
// When activating a window, we want to activate the window's current
|
||||
// workspace. Otherwise, find the workspace that we tapped on.
|
||||
let ws_matches = |ws: &Workspace<Mapped>| {
|
||||
if let Some(window) = &self.window {
|
||||
ws.has_window(window)
|
||||
} else if let Some(ws_id) = self.workspace_id {
|
||||
ws.id() == ws_id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let ws_idx = if let Some((Some(mon), ws_idx, _)) =
|
||||
layout.workspaces().find(|(_, _, ws)| ws_matches(ws))
|
||||
{
|
||||
// The workspace could've moved to a different output in the meantime.
|
||||
(*mon.output() == self.output).then_some(ws_idx)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(ws_idx) = ws_idx {
|
||||
layout.toggle_overview_to_workspace(ws_idx);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window) = self.window.as_ref() {
|
||||
layout.activate_window(window);
|
||||
}
|
||||
}
|
||||
GestureState::ViewOffset => {
|
||||
layout.view_offset_gesture_end(Some(false));
|
||||
}
|
||||
GestureState::WorkspaceSwitch => {
|
||||
layout.workspace_switch_gesture_end(Some(false));
|
||||
}
|
||||
GestureState::InteractiveMove => {
|
||||
layout.interactive_move_end(self.window.as_ref().unwrap());
|
||||
}
|
||||
};
|
||||
|
||||
state.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
impl TouchGrab<State> for TouchOverviewGrab {
|
||||
fn down(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &DownEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.down(data, None, event, seq);
|
||||
}
|
||||
|
||||
fn up(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &UpEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.up(data, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.motion(data, None, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||
let layout = &mut data.niri.layout;
|
||||
|
||||
// Check if we should become interactive move.
|
||||
if matches!(self.gesture, GestureState::Recognizing) {
|
||||
if let Some(window) = self.window.as_ref().filter(|win| win.alive()) {
|
||||
let passed = timestamp.saturating_sub(self.start_timestamp);
|
||||
if INTERACTIVE_MOVE_THRESHOLD <= passed
|
||||
&& layout.interactive_move_begin(
|
||||
window.clone(),
|
||||
&self.output,
|
||||
self.start_pos_within_output,
|
||||
)
|
||||
{
|
||||
self.gesture = GestureState::InteractiveMove;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should become a spatial scroll.
|
||||
if matches!(self.gesture, GestureState::Recognizing) {
|
||||
let c = event.location - self.start_data.location;
|
||||
|
||||
// Check if the gesture moved far enough to decide. Threshold copied from libadwaita.
|
||||
if c.x * c.x + c.y * c.y >= 16. * 16. {
|
||||
if let Some(ws_id) = self.workspace_id.filter(|_| c.x.abs() > c.y.abs()) {
|
||||
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(ws_id) {
|
||||
if ws.current_output() == Some(&self.output) {
|
||||
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
|
||||
self.gesture = GestureState::ViewOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(self.gesture, GestureState::Recognizing) {
|
||||
layout.workspace_switch_gesture_begin(&self.output, false);
|
||||
self.gesture = GestureState::WorkspaceSwitch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do nothing if still recognizing.
|
||||
if matches!(self.gesture, GestureState::Recognizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
let delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
|
||||
let ongoing = match self.gesture {
|
||||
GestureState::Recognizing => unreachable!(),
|
||||
GestureState::ViewOffset => layout
|
||||
.view_offset_gesture_update(-delta.x, timestamp, false)
|
||||
.is_some(),
|
||||
GestureState::WorkspaceSwitch => layout
|
||||
.workspace_switch_gesture_update(-delta.y, timestamp, false)
|
||||
.is_some(),
|
||||
GestureState::InteractiveMove => {
|
||||
let window = self.window.as_ref().unwrap();
|
||||
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
|
||||
let output = output.clone();
|
||||
data.niri.layout.interactive_move_update(
|
||||
window,
|
||||
delta,
|
||||
output,
|
||||
pos_within_output,
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if ongoing {
|
||||
data.niri.queue_redraw_all();
|
||||
} else {
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.frame(data, seq);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.cancel(data, seq);
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn shape(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &ShapeEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.shape(data, event, seq);
|
||||
}
|
||||
|
||||
fn orientation(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &OrientationEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.orientation(data, event, seq);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &TouchGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::touch::{
|
||||
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
|
||||
TouchGrab, TouchInnerHandle, UpEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{IsAlive, Logical, Point, Serial};
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct TouchResizeGrab {
|
||||
start_data: TouchGrabStartData<State>,
|
||||
window: Window,
|
||||
}
|
||||
|
||||
impl TouchResizeGrab {
|
||||
pub fn new(start_data: TouchGrabStartData<State>, window: Window) -> Self {
|
||||
Self { start_data, window }
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_resize_end(&self.window);
|
||||
}
|
||||
}
|
||||
|
||||
impl TouchGrab<State> for TouchResizeGrab {
|
||||
fn down(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &DownEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.down(data, None, event, seq);
|
||||
}
|
||||
|
||||
fn up(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &UpEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.up(data, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.motion(data, None, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.window.alive() {
|
||||
let delta = event.location - self.start_data.location;
|
||||
let ongoing = data
|
||||
.niri
|
||||
.layout
|
||||
.interactive_resize_update(&self.window, delta);
|
||||
if ongoing {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The resize is no longer ongoing.
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.frame(data, seq);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.cancel(data, seq);
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn shape(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &ShapeEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.shape(data, event, seq);
|
||||
}
|
||||
|
||||
fn orientation(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &OrientationEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.orientation(data, event, seq);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &TouchGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
+440
-140
@@ -1,6 +1,13 @@
|
||||
use std::io::ErrorKind;
|
||||
use std::iter::Peekable;
|
||||
use std::slice;
|
||||
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use niri_config::OutputName;
|
||||
use niri_ipc::socket::Socket;
|
||||
use niri_ipc::{
|
||||
LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response, Socket, Transform,
|
||||
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Overview, Request,
|
||||
Response, Transform, Window,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -12,30 +19,49 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
Msg::Version => Request::Version,
|
||||
Msg::Outputs => Request::Outputs,
|
||||
Msg::FocusedWindow => Request::FocusedWindow,
|
||||
Msg::FocusedOutput => Request::FocusedOutput,
|
||||
Msg::PickWindow => Request::PickWindow,
|
||||
Msg::PickColor => Request::PickColor,
|
||||
Msg::Action { action } => Request::Action(action.clone()),
|
||||
Msg::Output { output, action } => Request::Output {
|
||||
output: output.clone(),
|
||||
action: action.clone(),
|
||||
},
|
||||
Msg::Workspaces => Request::Workspaces,
|
||||
Msg::Windows => Request::Windows,
|
||||
Msg::Layers => Request::Layers,
|
||||
Msg::KeyboardLayouts => Request::KeyboardLayouts,
|
||||
Msg::EventStream => Request::EventStream,
|
||||
Msg::RequestError => Request::ReturnError,
|
||||
Msg::OverviewState => Request::OverviewState,
|
||||
};
|
||||
|
||||
let socket = Socket::connect().context("error connecting to the niri socket")?;
|
||||
let mut socket = Socket::connect().context("error connecting to the niri socket")?;
|
||||
|
||||
let reply = socket
|
||||
.send(request)
|
||||
.context("error communicating with niri")?;
|
||||
let result = socket.send(request);
|
||||
|
||||
let compositor_version = match reply {
|
||||
Err(_) if !matches!(msg, Msg::Version) => {
|
||||
// If we got an error, it might be that the CLI is a different version from the running
|
||||
// niri instance. Request the running instance version to compare and print a message.
|
||||
Socket::connect()
|
||||
.and_then(|socket| socket.send(Request::Version))
|
||||
.ok()
|
||||
// For errors that can be caused by a version mismatch between the running niri instance and
|
||||
// the niri msg CLI, we will try to fetch and compare the versions.
|
||||
let check_compositor_version = match &result {
|
||||
Err(err) => {
|
||||
// Response JSON parsing errors.
|
||||
matches!(
|
||||
err.kind(),
|
||||
ErrorKind::InvalidData | ErrorKind::UnexpectedEof
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
// Error returned from niri.
|
||||
Ok(Err(_)) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let compositor_version = if check_compositor_version && !matches!(msg, Msg::Version) {
|
||||
// Reconnect to support older niri versions with one request per connection.
|
||||
Socket::connect()
|
||||
.and_then(|mut socket| socket.send(Request::Version))
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Default SIGPIPE so that our prints don't panic on stdout closing.
|
||||
@@ -43,32 +69,31 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
||||
}
|
||||
|
||||
let response = reply.map_err(|err_msg| {
|
||||
// Check for CLI-server version mismatch to add helpful context.
|
||||
match compositor_version {
|
||||
Some(Ok(Response::Version(compositor_version))) => {
|
||||
let cli_version = version();
|
||||
if cli_version != compositor_version {
|
||||
eprintln!("Running niri compositor has a different version from the niri CLI:");
|
||||
eprintln!("Compositor version: {compositor_version}");
|
||||
eprintln!("CLI version: {cli_version}");
|
||||
eprintln!("Did you forget to restart niri after an update?");
|
||||
eprintln!();
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
eprintln!("Unable to get the running niri compositor version.");
|
||||
// Check for CLI-server version mismatch to add helpful context.
|
||||
match compositor_version {
|
||||
Some(Ok(Response::Version(compositor_version))) => {
|
||||
let cli_version = version();
|
||||
if cli_version != compositor_version {
|
||||
eprintln!("Running niri compositor has a different version from the niri CLI:");
|
||||
eprintln!("Compositor version: {compositor_version}");
|
||||
eprintln!("CLI version: {cli_version}");
|
||||
eprintln!("Did you forget to restart niri after an update?");
|
||||
eprintln!();
|
||||
}
|
||||
None => {
|
||||
// Communication error, or the original request was already a version request.
|
||||
// Don't add irrelevant context.
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
eprintln!("Unable to get the running niri compositor version.");
|
||||
eprintln!("Did you forget to restart niri after an update?");
|
||||
eprintln!();
|
||||
}
|
||||
None => {
|
||||
// Communication error, or the original request was already a version request, or the
|
||||
// original request had succeeded. Don't add irrelevant context.
|
||||
}
|
||||
}
|
||||
|
||||
anyhow!(err_msg).context("niri returned an error")
|
||||
})?;
|
||||
let reply = result.context("error communicating with niri")?;
|
||||
let response = reply.map_err(|err_msg| anyhow!(err_msg).context("niri returned an error"))?;
|
||||
|
||||
match msg {
|
||||
Msg::RequestError => {
|
||||
@@ -113,100 +138,14 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut outputs = outputs.into_iter().collect::<Vec<_>>();
|
||||
outputs.sort_unstable_by(|a, b| a.0.cmp(&b.0));
|
||||
let mut outputs = outputs
|
||||
.into_values()
|
||||
.map(|out| (OutputName::from_ipc_output(&out), out))
|
||||
.collect::<Vec<_>>();
|
||||
outputs.sort_unstable_by(|a, b| a.0.compare(&b.0));
|
||||
|
||||
for (connector, output) in outputs.into_iter() {
|
||||
let Output {
|
||||
name,
|
||||
make,
|
||||
model,
|
||||
physical_size,
|
||||
modes,
|
||||
current_mode,
|
||||
vrr_supported,
|
||||
vrr_enabled,
|
||||
logical,
|
||||
} = output;
|
||||
|
||||
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
|
||||
|
||||
if let Some(current) = current_mode {
|
||||
let mode = *modes
|
||||
.get(current)
|
||||
.context("invalid response: current mode does not exist")?;
|
||||
let Mode {
|
||||
width,
|
||||
height,
|
||||
refresh_rate,
|
||||
is_preferred,
|
||||
} = mode;
|
||||
let refresh = refresh_rate as f64 / 1000.;
|
||||
let preferred = if is_preferred { " (preferred)" } else { "" };
|
||||
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz{preferred}");
|
||||
} else {
|
||||
println!(" Disabled");
|
||||
}
|
||||
|
||||
if vrr_supported {
|
||||
let enabled = if vrr_enabled { "enabled" } else { "disabled" };
|
||||
println!(" Variable refresh rate: supported, {enabled}");
|
||||
} else {
|
||||
println!(" Variable refresh rate: not supported");
|
||||
}
|
||||
|
||||
if let Some((width, height)) = physical_size {
|
||||
println!(" Physical size: {width}x{height} mm");
|
||||
} else {
|
||||
println!(" Physical size: unknown");
|
||||
}
|
||||
|
||||
if let Some(logical) = logical {
|
||||
let LogicalOutput {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
scale,
|
||||
transform,
|
||||
} = logical;
|
||||
println!(" Logical position: {x}, {y}");
|
||||
println!(" Logical size: {width}x{height}");
|
||||
println!(" Scale: {scale}");
|
||||
|
||||
let transform = match transform {
|
||||
Transform::Normal => "normal",
|
||||
Transform::_90 => "90° counter-clockwise",
|
||||
Transform::_180 => "180°",
|
||||
Transform::_270 => "270° counter-clockwise",
|
||||
Transform::Flipped => "flipped horizontally",
|
||||
Transform::Flipped90 => "90° counter-clockwise, flipped horizontally",
|
||||
Transform::Flipped180 => "flipped vertically",
|
||||
Transform::Flipped270 => "270° counter-clockwise, flipped horizontally",
|
||||
};
|
||||
println!(" Transform: {transform}");
|
||||
}
|
||||
|
||||
println!(" Available modes:");
|
||||
for (idx, mode) in modes.into_iter().enumerate() {
|
||||
let Mode {
|
||||
width,
|
||||
height,
|
||||
refresh_rate,
|
||||
is_preferred,
|
||||
} = mode;
|
||||
let refresh = refresh_rate as f64 / 1000.;
|
||||
|
||||
let is_current = Some(idx) == current_mode;
|
||||
let qualifier = match (is_current, is_preferred) {
|
||||
(true, true) => " (current, preferred)",
|
||||
(true, false) => " (current)",
|
||||
(false, true) => " (preferred)",
|
||||
(false, false) => "",
|
||||
};
|
||||
|
||||
println!(" {width}x{height}@{refresh:.3}{qualifier}");
|
||||
}
|
||||
for (_name, output) in outputs.into_iter() {
|
||||
print_output(output)?;
|
||||
println!();
|
||||
}
|
||||
}
|
||||
@@ -222,23 +161,147 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
if let Some(window) = window {
|
||||
println!("Focused window:");
|
||||
|
||||
if let Some(title) = window.title {
|
||||
println!(" Title: \"{title}\"");
|
||||
} else {
|
||||
println!(" Title: (unset)");
|
||||
}
|
||||
|
||||
if let Some(app_id) = window.app_id {
|
||||
println!(" App ID: \"{app_id}\"");
|
||||
} else {
|
||||
println!(" App ID: (unset)");
|
||||
}
|
||||
print_window(&window);
|
||||
} else {
|
||||
println!("No window is focused.");
|
||||
}
|
||||
}
|
||||
Msg::Windows => {
|
||||
let Response::Windows(mut windows) = response else {
|
||||
bail!("unexpected response: expected Windows, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let windows =
|
||||
serde_json::to_string(&windows).context("error formatting response")?;
|
||||
println!("{windows}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
windows.sort_unstable_by(|a, b| a.id.cmp(&b.id));
|
||||
|
||||
for window in windows {
|
||||
print_window(&window);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
Msg::Layers => {
|
||||
let Response::Layers(mut layers) = response else {
|
||||
bail!("unexpected response: expected Layers, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let layers = serde_json::to_string(&layers).context("error formatting response")?;
|
||||
println!("{layers}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
layers.sort_by(|a, b| {
|
||||
Ord::cmp(&a.output, &b.output)
|
||||
.then_with(|| Ord::cmp(&a.layer, &b.layer))
|
||||
.then_with(|| Ord::cmp(&a.namespace, &b.namespace))
|
||||
});
|
||||
let mut iter = layers.iter().peekable();
|
||||
|
||||
let print = |surface: &niri_ipc::LayerSurface| {
|
||||
println!(" Surface:");
|
||||
println!(" Namespace: \"{}\"", &surface.namespace);
|
||||
|
||||
let interactivity = match surface.keyboard_interactivity {
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::None => "none",
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::Exclusive => "exclusive",
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::OnDemand => "on-demand",
|
||||
};
|
||||
println!(" Keyboard interactivity: {interactivity}");
|
||||
};
|
||||
|
||||
let print_layer = |iter: &mut Peekable<slice::Iter<niri_ipc::LayerSurface>>,
|
||||
output: &str,
|
||||
layer| {
|
||||
let mut empty = true;
|
||||
while let Some(surface) = iter.next_if(|s| s.output == output && s.layer == layer) {
|
||||
empty = false;
|
||||
println!();
|
||||
print(surface);
|
||||
}
|
||||
if empty {
|
||||
println!(" (empty)\n");
|
||||
} else {
|
||||
println!();
|
||||
}
|
||||
};
|
||||
|
||||
while let Some(surface) = iter.peek() {
|
||||
let output = &surface.output;
|
||||
println!("Output \"{output}\":");
|
||||
|
||||
print!(" Background layer:");
|
||||
print_layer(&mut iter, output, niri_ipc::Layer::Background);
|
||||
|
||||
print!(" Bottom layer:");
|
||||
print_layer(&mut iter, output, niri_ipc::Layer::Bottom);
|
||||
|
||||
print!(" Top layer:");
|
||||
print_layer(&mut iter, output, niri_ipc::Layer::Top);
|
||||
|
||||
print!(" Overlay layer:");
|
||||
print_layer(&mut iter, output, niri_ipc::Layer::Overlay);
|
||||
}
|
||||
}
|
||||
Msg::FocusedOutput => {
|
||||
let Response::FocusedOutput(output) = response else {
|
||||
bail!("unexpected response: expected FocusedOutput, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let output = serde_json::to_string(&output).context("error formatting response")?;
|
||||
println!("{output}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(output) = output {
|
||||
print_output(output)?;
|
||||
} else {
|
||||
println!("No output is focused.");
|
||||
}
|
||||
}
|
||||
Msg::PickWindow => {
|
||||
let Response::PickedWindow(window) = response else {
|
||||
bail!("unexpected response: expected PickedWindow, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let window = serde_json::to_string(&window).context("error formatting response")?;
|
||||
println!("{window}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(window) = window {
|
||||
print_window(&window);
|
||||
} else {
|
||||
println!("No window selected.");
|
||||
}
|
||||
}
|
||||
Msg::PickColor => {
|
||||
let Response::PickedColor(color) = response else {
|
||||
bail!("unexpected response: expected PickedColor, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let color = serde_json::to_string(&color).context("error formatting response")?;
|
||||
println!("{color}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(color) = color {
|
||||
let [r, g, b] = color.rgb.map(|v| (v.clamp(0., 1.) * 255.).round() as u8);
|
||||
|
||||
println!("Picked color: rgb({r}, {g}, {b})",);
|
||||
println!("Hex: #{:02x}{:02x}{:02x}", r, g, b);
|
||||
} else {
|
||||
println!("No color was picked.");
|
||||
}
|
||||
}
|
||||
Msg::Action { .. } => {
|
||||
let Response::Handled = response else {
|
||||
bail!("unexpected response: expected Handled, got {response:?}");
|
||||
@@ -309,7 +372,244 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
println!("{is_active}{idx}{name}");
|
||||
}
|
||||
}
|
||||
Msg::KeyboardLayouts => {
|
||||
let Response::KeyboardLayouts(response) = response else {
|
||||
bail!("unexpected response: expected KeyboardLayouts, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let response =
|
||||
serde_json::to_string(&response).context("error formatting response")?;
|
||||
println!("{response}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let KeyboardLayouts { names, current_idx } = response;
|
||||
let current_idx = usize::from(current_idx);
|
||||
|
||||
println!("Keyboard layouts:");
|
||||
for (idx, name) in names.iter().enumerate() {
|
||||
let is_active = if idx == current_idx { " * " } else { " " };
|
||||
println!("{is_active}{idx} {name}");
|
||||
}
|
||||
}
|
||||
Msg::EventStream => {
|
||||
let Response::Handled = response else {
|
||||
bail!("unexpected response: expected Handled, got {response:?}");
|
||||
};
|
||||
|
||||
if !json {
|
||||
println!("Started reading events.");
|
||||
}
|
||||
|
||||
let mut read_event = socket.read_events();
|
||||
loop {
|
||||
let event = read_event().context("error reading event from niri")?;
|
||||
|
||||
if json {
|
||||
let event = serde_json::to_string(&event).context("error formatting event")?;
|
||||
println!("{event}");
|
||||
continue;
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::WorkspacesChanged { workspaces } => {
|
||||
println!("Workspaces changed: {workspaces:?}");
|
||||
}
|
||||
Event::WorkspaceUrgencyChanged { id, urgent } => {
|
||||
println!("Workspace {id}: urgency changed to {urgent}");
|
||||
}
|
||||
Event::WorkspaceActivated { id, focused } => {
|
||||
let word = if focused { "focused" } else { "activated" };
|
||||
println!("Workspace {word}: {id}");
|
||||
}
|
||||
Event::WorkspaceActiveWindowChanged {
|
||||
workspace_id,
|
||||
active_window_id,
|
||||
} => {
|
||||
println!(
|
||||
"Workspace {workspace_id}: \
|
||||
active window changed to {active_window_id:?}"
|
||||
);
|
||||
}
|
||||
Event::WindowsChanged { windows } => {
|
||||
println!("Windows changed: {windows:?}");
|
||||
}
|
||||
Event::WindowOpenedOrChanged { window } => {
|
||||
println!("Window opened or changed: {window:?}");
|
||||
}
|
||||
Event::WindowClosed { id } => {
|
||||
println!("Window closed: {id}");
|
||||
}
|
||||
Event::WindowFocusChanged { id } => {
|
||||
println!("Window focus changed: {id:?}");
|
||||
}
|
||||
Event::WindowUrgencyChanged { id, urgent } => {
|
||||
println!("Window {id}: urgency changed to {urgent}");
|
||||
}
|
||||
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
|
||||
println!("Keyboard layouts changed: {keyboard_layouts:?}");
|
||||
}
|
||||
Event::KeyboardLayoutSwitched { idx } => {
|
||||
println!("Keyboard layout switched: {idx}");
|
||||
}
|
||||
Event::OverviewOpenedOrClosed { is_open: opened } => {
|
||||
println!("Overview toggled: {opened}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg::OverviewState => {
|
||||
let Response::OverviewState(response) = response else {
|
||||
bail!("unexpected response: expected Overview, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let response =
|
||||
serde_json::to_string(&response).context("error formatting response")?;
|
||||
println!("{response}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Overview { is_open } = response;
|
||||
if is_open {
|
||||
println!("Overview is open.");
|
||||
} else {
|
||||
println!("Overview is closed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_output(output: Output) -> anyhow::Result<()> {
|
||||
let Output {
|
||||
name,
|
||||
make,
|
||||
model,
|
||||
serial,
|
||||
physical_size,
|
||||
modes,
|
||||
current_mode,
|
||||
vrr_supported,
|
||||
vrr_enabled,
|
||||
logical,
|
||||
} = output;
|
||||
|
||||
let serial = serial.as_deref().unwrap_or("Unknown");
|
||||
println!(r#"Output "{make} {model} {serial}" ({name})"#);
|
||||
|
||||
if let Some(current) = current_mode {
|
||||
let mode = *modes
|
||||
.get(current)
|
||||
.context("invalid response: current mode does not exist")?;
|
||||
let Mode {
|
||||
width,
|
||||
height,
|
||||
refresh_rate,
|
||||
is_preferred,
|
||||
} = mode;
|
||||
let refresh = refresh_rate as f64 / 1000.;
|
||||
let preferred = if is_preferred { " (preferred)" } else { "" };
|
||||
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz{preferred}");
|
||||
} else {
|
||||
println!(" Disabled");
|
||||
}
|
||||
|
||||
if vrr_supported {
|
||||
let enabled = if vrr_enabled { "enabled" } else { "disabled" };
|
||||
println!(" Variable refresh rate: supported, {enabled}");
|
||||
} else {
|
||||
println!(" Variable refresh rate: not supported");
|
||||
}
|
||||
|
||||
if let Some((width, height)) = physical_size {
|
||||
println!(" Physical size: {width}x{height} mm");
|
||||
} else {
|
||||
println!(" Physical size: unknown");
|
||||
}
|
||||
|
||||
if let Some(logical) = logical {
|
||||
let LogicalOutput {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
scale,
|
||||
transform,
|
||||
} = logical;
|
||||
println!(" Logical position: {x}, {y}");
|
||||
println!(" Logical size: {width}x{height}");
|
||||
println!(" Scale: {scale}");
|
||||
|
||||
let transform = match transform {
|
||||
Transform::Normal => "normal",
|
||||
Transform::_90 => "90° counter-clockwise",
|
||||
Transform::_180 => "180°",
|
||||
Transform::_270 => "270° counter-clockwise",
|
||||
Transform::Flipped => "flipped horizontally",
|
||||
Transform::Flipped90 => "90° counter-clockwise, flipped horizontally",
|
||||
Transform::Flipped180 => "flipped vertically",
|
||||
Transform::Flipped270 => "270° counter-clockwise, flipped horizontally",
|
||||
};
|
||||
println!(" Transform: {transform}");
|
||||
}
|
||||
|
||||
println!(" Available modes:");
|
||||
for (idx, mode) in modes.into_iter().enumerate() {
|
||||
let Mode {
|
||||
width,
|
||||
height,
|
||||
refresh_rate,
|
||||
is_preferred,
|
||||
} = mode;
|
||||
let refresh = refresh_rate as f64 / 1000.;
|
||||
|
||||
let is_current = Some(idx) == current_mode;
|
||||
let qualifier = match (is_current, is_preferred) {
|
||||
(true, true) => " (current, preferred)",
|
||||
(true, false) => " (current)",
|
||||
(false, true) => " (preferred)",
|
||||
(false, false) => "",
|
||||
};
|
||||
|
||||
println!(" {width}x{height}@{refresh:.3}{qualifier}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_window(window: &Window) {
|
||||
let focused = if window.is_focused { " (focused)" } else { "" };
|
||||
let urgent = if window.is_urgent { " (urgent)" } else { "" };
|
||||
println!("Window ID {}:{focused}{urgent}", window.id);
|
||||
|
||||
if let Some(title) = &window.title {
|
||||
println!(" Title: \"{title}\"");
|
||||
} else {
|
||||
println!(" Title: (unset)");
|
||||
}
|
||||
|
||||
if let Some(app_id) = &window.app_id {
|
||||
println!(" App ID: \"{app_id}\"");
|
||||
} else {
|
||||
println!(" App ID: (unset)");
|
||||
}
|
||||
|
||||
println!(
|
||||
" Is floating: {}",
|
||||
if window.is_floating { "yes" } else { "no" }
|
||||
);
|
||||
|
||||
if let Some(pid) = window.pid {
|
||||
println!(" PID: {pid}");
|
||||
} else {
|
||||
println!(" PID: (unknown)");
|
||||
}
|
||||
|
||||
if let Some(workspace_id) = window.workspace_id {
|
||||
println!(" Workspace ID: {workspace_id}");
|
||||
} else {
|
||||
println!(" Workspace ID: (none)");
|
||||
}
|
||||
}
|
||||
|
||||
+606
-78
@@ -1,71 +1,145 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{env, io, process};
|
||||
|
||||
use anyhow::Context;
|
||||
use async_channel::{Receiver, Sender, TrySendError};
|
||||
use calloop::futures::Scheduler;
|
||||
use calloop::io::Async;
|
||||
use directories::BaseDirs;
|
||||
use futures_util::io::{AsyncReadExt, BufReader};
|
||||
use futures_util::{AsyncBufReadExt, AsyncWriteExt};
|
||||
use niri_ipc::{OutputConfigChanged, Reply, Request, Response};
|
||||
use smithay::desktop::Window;
|
||||
use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, FutureExt as _};
|
||||
use niri_config::OutputName;
|
||||
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
|
||||
use niri_ipc::{
|
||||
Event, KeyboardLayouts, OutputConfigChanged, Overview, Reply, Request, Response, Workspace,
|
||||
};
|
||||
use smithay::desktop::layer_map_for_output;
|
||||
use smithay::input::pointer::{
|
||||
CursorIcon, CursorImageStatus, Focus, GrabStartData as PointerGrabStartData,
|
||||
};
|
||||
use smithay::reexports::calloop::generic::Generic;
|
||||
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||
use smithay::reexports::rustix::fs::unlink;
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||
use smithay::utils::SERIAL_COUNTER;
|
||||
use smithay::wayland::shell::wlr_layer::{KeyboardInteractivity, Layer};
|
||||
|
||||
use crate::backend::IpcOutputMap;
|
||||
use crate::input::pick_window_grab::PickWindowGrab;
|
||||
use crate::layout::workspace::WorkspaceId;
|
||||
use crate::niri::State;
|
||||
use crate::utils::version;
|
||||
use crate::utils::{version, with_toplevel_role};
|
||||
use crate::window::Mapped;
|
||||
|
||||
// If an event stream client fails to read events fast enough that we accumulate more than this
|
||||
// number in our buffer, we drop that event stream client.
|
||||
const EVENT_STREAM_BUFFER_SIZE: usize = 64;
|
||||
|
||||
pub struct IpcServer {
|
||||
pub socket_path: PathBuf,
|
||||
/// Path to the IPC socket.
|
||||
///
|
||||
/// This is `None` when creating `IpcServer` without a socket.
|
||||
pub socket_path: Option<PathBuf>,
|
||||
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
|
||||
event_stream_state: Rc<RefCell<EventStreamState>>,
|
||||
}
|
||||
|
||||
struct ClientCtx {
|
||||
event_loop: LoopHandle<'static, State>,
|
||||
scheduler: Scheduler<()>,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
ipc_focused_window: Arc<Mutex<Option<Window>>>,
|
||||
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
|
||||
event_stream_state: Rc<RefCell<EventStreamState>>,
|
||||
}
|
||||
|
||||
struct EventStreamClient {
|
||||
events: Receiver<Event>,
|
||||
disconnect: Receiver<()>,
|
||||
write: Box<dyn AsyncWrite + Unpin>,
|
||||
}
|
||||
|
||||
struct EventStreamSender {
|
||||
events: Sender<Event>,
|
||||
disconnect: Sender<()>,
|
||||
}
|
||||
|
||||
impl IpcServer {
|
||||
pub fn start(
|
||||
event_loop: &LoopHandle<'static, State>,
|
||||
wayland_socket_name: &str,
|
||||
wayland_socket_name: Option<&OsStr>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let _span = tracy_client::span!("Ipc::start");
|
||||
|
||||
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
|
||||
let mut socket_path = socket_dir();
|
||||
socket_path.push(socket_name);
|
||||
let socket_path = if let Some(wayland_socket_name) = wayland_socket_name {
|
||||
let wayland_socket_name = wayland_socket_name.to_string_lossy();
|
||||
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
|
||||
let mut socket_path = socket_dir();
|
||||
socket_path.push(socket_name);
|
||||
|
||||
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.context("error setting socket to non-blocking")?;
|
||||
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.context("error setting socket to non-blocking")?;
|
||||
|
||||
let source = Generic::new(listener, Interest::READ, Mode::Level);
|
||||
event_loop
|
||||
.insert_source(source, |_, socket, state| {
|
||||
match socket.accept() {
|
||||
Ok((stream, _)) => on_new_ipc_client(state, stream),
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
|
||||
Err(e) => return Err(e),
|
||||
let source = Generic::new(listener, Interest::READ, Mode::Level);
|
||||
event_loop
|
||||
.insert_source(source, |_, socket, state| {
|
||||
match socket.accept() {
|
||||
Ok((stream, _)) => on_new_ipc_client(state, stream),
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
Ok(PostAction::Continue)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Some(socket_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
socket_path,
|
||||
event_streams: Rc::new(RefCell::new(Vec::new())),
|
||||
event_stream_state: Rc::new(RefCell::new(EventStreamState::default())),
|
||||
})
|
||||
}
|
||||
|
||||
fn send_event(&self, event: Event) {
|
||||
let mut streams = self.event_streams.borrow_mut();
|
||||
let mut to_remove = Vec::new();
|
||||
for (idx, stream) in streams.iter_mut().enumerate() {
|
||||
match stream.events.try_send(event.clone()) {
|
||||
Ok(()) => (),
|
||||
Err(TrySendError::Closed(_)) => to_remove.push(idx),
|
||||
Err(TrySendError::Full(_)) => {
|
||||
warn!(
|
||||
"disconnecting IPC event stream client \
|
||||
because it is reading events too slowly"
|
||||
);
|
||||
to_remove.push(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(PostAction::Continue)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Ok(Self { socket_path })
|
||||
for idx in to_remove.into_iter().rev() {
|
||||
let stream = streams.swap_remove(idx);
|
||||
let _ = stream.disconnect.send_blocking(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for IpcServer {
|
||||
fn drop(&mut self) {
|
||||
let _ = unlink(&self.socket_path);
|
||||
if let Some(socket_path) = &self.socket_path {
|
||||
let _ = unlink(socket_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,10 +163,14 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
|
||||
}
|
||||
};
|
||||
|
||||
let ipc_server = state.niri.ipc_server.as_ref().unwrap();
|
||||
|
||||
let ctx = ClientCtx {
|
||||
event_loop: state.niri.event_loop.clone(),
|
||||
scheduler: state.niri.scheduler.clone(),
|
||||
ipc_outputs: state.backend.ipc_outputs(),
|
||||
ipc_focused_window: state.niri.ipc_focused_window.clone(),
|
||||
event_streams: ipc_server.event_streams.clone(),
|
||||
event_stream_state: ipc_server.event_stream_state.clone(),
|
||||
};
|
||||
|
||||
let future = async move {
|
||||
@@ -105,36 +183,88 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow::Result<()> {
|
||||
async fn handle_client(ctx: ClientCtx, stream: Async<'static, UnixStream>) -> anyhow::Result<()> {
|
||||
let (read, mut write) = stream.split();
|
||||
let mut buf = String::new();
|
||||
let mut read = BufReader::new(read);
|
||||
|
||||
// Read a single line to allow extensibility in the future to keep reading.
|
||||
BufReader::new(read)
|
||||
.read_line(&mut buf)
|
||||
.await
|
||||
.context("error reading request")?;
|
||||
loop {
|
||||
// Don't keep buf around to avoid clients wasting RAM by filling it with bogus data.
|
||||
let mut buf = Vec::new();
|
||||
let res = read.read_until(b'\n', &mut buf).await;
|
||||
match res {
|
||||
Ok(0) => return Ok(()),
|
||||
Ok(_) => (),
|
||||
// Normal client disconnection.
|
||||
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
|
||||
Err(err) => {
|
||||
return Err(err).context("error reading request");
|
||||
}
|
||||
}
|
||||
|
||||
let request = serde_json::from_str(&buf)
|
||||
.context("error parsing request")
|
||||
.map_err(|err| err.to_string());
|
||||
let requested_error = matches!(request, Ok(Request::ReturnError));
|
||||
let request = serde_json::from_slice(&buf)
|
||||
.context("error parsing request")
|
||||
.map_err(|err| err.to_string());
|
||||
let requested_error = matches!(request, Ok(Request::ReturnError));
|
||||
let requested_event_stream = matches!(request, Ok(Request::EventStream));
|
||||
|
||||
let reply = match request {
|
||||
Ok(request) => process(&ctx, request).await,
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
let reply = match request {
|
||||
Ok(request) => process(&ctx, request).await,
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
if let Err(err) = &reply {
|
||||
if !requested_error {
|
||||
warn!("error processing IPC request: {err:?}");
|
||||
if let Err(err) = &reply {
|
||||
if !requested_error {
|
||||
warn!("error processing IPC request: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
buf.clear();
|
||||
serde_json::to_writer(&mut buf, &reply).context("error formatting reply")?;
|
||||
buf.push(b'\n');
|
||||
write.write_all(&buf).await.context("error writing reply")?;
|
||||
|
||||
if requested_event_stream {
|
||||
let (events_tx, events_rx) = async_channel::bounded(EVENT_STREAM_BUFFER_SIZE);
|
||||
let (disconnect_tx, disconnect_rx) = async_channel::bounded(1);
|
||||
|
||||
// Spawn a task for the client.
|
||||
let client = EventStreamClient {
|
||||
events: events_rx,
|
||||
disconnect: disconnect_rx,
|
||||
write: Box::new(write) as _,
|
||||
};
|
||||
let future = async move {
|
||||
if let Err(err) = handle_event_stream_client(client).await {
|
||||
warn!("error handling IPC event stream client: {err:?}");
|
||||
}
|
||||
};
|
||||
if let Err(err) = ctx.scheduler.schedule(future) {
|
||||
warn!("error scheduling IPC event stream future: {err:?}");
|
||||
}
|
||||
|
||||
// Send the initial state.
|
||||
{
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
for event in state.replicate() {
|
||||
events_tx
|
||||
.try_send(event)
|
||||
.expect("initial event burst had more events than buffer size");
|
||||
}
|
||||
}
|
||||
|
||||
// Add it to the list.
|
||||
{
|
||||
let mut streams = ctx.event_streams.borrow_mut();
|
||||
let sender = EventStreamSender {
|
||||
events: events_tx,
|
||||
disconnect: disconnect_tx,
|
||||
};
|
||||
streams.push(sender);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let buf = serde_json::to_vec(&reply).context("error formatting reply")?;
|
||||
write.write_all(&buf).await.context("error writing reply")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
@@ -143,33 +273,118 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
Request::Version => Response::Version(version()),
|
||||
Request::Outputs => {
|
||||
let ipc_outputs = ctx.ipc_outputs.lock().unwrap().clone();
|
||||
Response::Outputs(ipc_outputs)
|
||||
let outputs = ipc_outputs.values().cloned().map(|o| (o.name.clone(), o));
|
||||
Response::Outputs(outputs.collect())
|
||||
}
|
||||
Request::Workspaces => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let workspaces = state.workspaces.workspaces.values().cloned().collect();
|
||||
Response::Workspaces(workspaces)
|
||||
}
|
||||
Request::Windows => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let windows = state.windows.windows.values().cloned().collect();
|
||||
Response::Windows(windows)
|
||||
}
|
||||
Request::Layers => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
let mut layers = Vec::new();
|
||||
for output in state.niri.global_space.outputs() {
|
||||
let name = output.name();
|
||||
for surface in layer_map_for_output(output).layers() {
|
||||
let layer = match surface.layer() {
|
||||
Layer::Background => niri_ipc::Layer::Background,
|
||||
Layer::Bottom => niri_ipc::Layer::Bottom,
|
||||
Layer::Top => niri_ipc::Layer::Top,
|
||||
Layer::Overlay => niri_ipc::Layer::Overlay,
|
||||
};
|
||||
let keyboard_interactivity =
|
||||
match surface.cached_state().keyboard_interactivity {
|
||||
KeyboardInteractivity::None => {
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::None
|
||||
}
|
||||
KeyboardInteractivity::Exclusive => {
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::Exclusive
|
||||
}
|
||||
KeyboardInteractivity::OnDemand => {
|
||||
niri_ipc::LayerSurfaceKeyboardInteractivity::OnDemand
|
||||
}
|
||||
};
|
||||
|
||||
layers.push(niri_ipc::LayerSurface {
|
||||
namespace: surface.namespace().to_owned(),
|
||||
output: name.clone(),
|
||||
layer,
|
||||
keyboard_interactivity,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let _ = tx.send_blocking(layers);
|
||||
});
|
||||
let result = rx.recv().await;
|
||||
let layers = result.map_err(|_| String::from("error getting layers info"))?;
|
||||
Response::Layers(layers)
|
||||
}
|
||||
Request::KeyboardLayouts => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let layout = state.keyboard_layouts.keyboard_layouts.clone();
|
||||
let layout = layout.expect("keyboard layouts should be set at startup");
|
||||
Response::KeyboardLayouts(layout)
|
||||
}
|
||||
Request::FocusedWindow => {
|
||||
let window = ctx.ipc_focused_window.lock().unwrap().clone();
|
||||
let window = window.map(|window| {
|
||||
let wl_surface = window.toplevel().expect("no X11 support").wl_surface();
|
||||
with_states(wl_surface, |states| {
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
niri_ipc::Window {
|
||||
title: role.title.clone(),
|
||||
app_id: role.app_id.clone(),
|
||||
}
|
||||
})
|
||||
});
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let windows = &state.windows.windows;
|
||||
let window = windows.values().find(|win| win.is_focused).cloned();
|
||||
Response::FocusedWindow(window)
|
||||
}
|
||||
Request::PickWindow => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
let pointer = state.niri.seat.get_pointer().unwrap();
|
||||
let start_data = PointerGrabStartData {
|
||||
focus: None,
|
||||
button: 0,
|
||||
location: pointer.current_location(),
|
||||
};
|
||||
let grab = PickWindowGrab::new(start_data);
|
||||
// The `WindowPickGrab` ungrab handler will cancel the previous ongoing pick, if
|
||||
// any.
|
||||
pointer.set_grab(state, grab, SERIAL_COUNTER.next_serial(), Focus::Clear);
|
||||
state.niri.pick_window = Some(tx);
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Crosshair));
|
||||
// Redraw to update the cursor.
|
||||
state.niri.queue_redraw_all();
|
||||
});
|
||||
let result = rx.recv().await;
|
||||
let id = result.map_err(|_| String::from("error getting picked window info"))?;
|
||||
let window = id.and_then(|id| {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
state.windows.windows.get(&id.get()).cloned()
|
||||
});
|
||||
Response::PickedWindow(window)
|
||||
}
|
||||
Request::PickColor => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
state.handle_pick_color(tx);
|
||||
});
|
||||
let result = rx.recv().await;
|
||||
let color = result.map_err(|_| String::from("error getting picked color"))?;
|
||||
Response::PickedColor(color)
|
||||
}
|
||||
Request::Action(action) => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
|
||||
let action = niri_config::Action::from(action);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
// Make sure some logic like workspace clean-up has a chance to run before doing
|
||||
// actions.
|
||||
state.niri.advance_animations();
|
||||
state.do_action(action, false);
|
||||
let _ = tx.send_blocking(());
|
||||
});
|
||||
@@ -183,8 +398,8 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
Request::Output { output, action } => {
|
||||
let ipc_outputs = ctx.ipc_outputs.lock().unwrap();
|
||||
let found = ipc_outputs
|
||||
.keys()
|
||||
.any(|name| name.eq_ignore_ascii_case(&output));
|
||||
.values()
|
||||
.any(|o| OutputName::from_ipc_output(o).matches(&output));
|
||||
let response = if found {
|
||||
OutputConfigChanged::Applied
|
||||
} else {
|
||||
@@ -198,17 +413,330 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
|
||||
Response::OutputConfigChanged(response)
|
||||
}
|
||||
Request::Workspaces => {
|
||||
Request::FocusedOutput => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
let workspaces = state.niri.layout.ipc_workspaces();
|
||||
let _ = tx.send_blocking(workspaces);
|
||||
let active_output = state
|
||||
.niri
|
||||
.layout
|
||||
.active_output()
|
||||
.map(|output| output.name());
|
||||
|
||||
let output = active_output.and_then(|active_output| {
|
||||
state
|
||||
.backend
|
||||
.ipc_outputs()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.values()
|
||||
.find(|o| o.name == active_output)
|
||||
.cloned()
|
||||
});
|
||||
|
||||
let _ = tx.send_blocking(output);
|
||||
});
|
||||
let result = rx.recv().await;
|
||||
let workspaces = result.map_err(|_| String::from("error getting workspace info"))?;
|
||||
Response::Workspaces(workspaces)
|
||||
let output = result.map_err(|_| String::from("error getting active output info"))?;
|
||||
Response::FocusedOutput(output)
|
||||
}
|
||||
Request::EventStream => Response::Handled,
|
||||
Request::OverviewState => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let is_open = state.overview.is_open;
|
||||
Response::OverviewState(Overview { is_open })
|
||||
}
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_event_stream_client(client: EventStreamClient) -> anyhow::Result<()> {
|
||||
let EventStreamClient {
|
||||
events,
|
||||
disconnect,
|
||||
mut write,
|
||||
} = client;
|
||||
|
||||
while let Ok(event) = events.recv().await {
|
||||
let mut buf = serde_json::to_vec(&event).context("error formatting event")?;
|
||||
buf.push(b'\n');
|
||||
|
||||
let res = select_biased! {
|
||||
_ = disconnect.recv().fuse() => return Ok(()),
|
||||
res = write.write_all(&buf).fuse() => res,
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(()) => (),
|
||||
// Normal client disconnection.
|
||||
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
|
||||
res @ Err(_) => res.context("error writing event")?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_ipc::Window {
|
||||
with_toplevel_role(mapped.toplevel(), |role| niri_ipc::Window {
|
||||
id: mapped.id().get(),
|
||||
title: role.title.clone(),
|
||||
app_id: role.app_id.clone(),
|
||||
pid: mapped.credentials().map(|c| c.pid),
|
||||
workspace_id: workspace_id.map(|id| id.get()),
|
||||
is_focused: mapped.is_focused(),
|
||||
is_floating: mapped.is_floating(),
|
||||
is_urgent: mapped.is_urgent(),
|
||||
})
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn ipc_keyboard_layouts_changed(&mut self) {
|
||||
let keyboard = self.niri.seat.get_keyboard().unwrap();
|
||||
let keyboard_layouts = keyboard.with_xkb_state(self, |context| {
|
||||
let xkb = context.xkb().lock().unwrap();
|
||||
let layouts = xkb.layouts();
|
||||
KeyboardLayouts {
|
||||
names: layouts
|
||||
.map(|layout| xkb.layout_name(layout).to_owned())
|
||||
.collect(),
|
||||
current_idx: xkb.active_layout().0 as u8,
|
||||
}
|
||||
});
|
||||
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.keyboard_layouts;
|
||||
|
||||
let event = Event::KeyboardLayoutsChanged { keyboard_layouts };
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
|
||||
pub fn ipc_refresh_keyboard_layout_index(&mut self) {
|
||||
let keyboard = self.niri.seat.get_keyboard().unwrap();
|
||||
let idx = keyboard.with_xkb_state(self, |context| {
|
||||
let xkb = context.xkb().lock().unwrap();
|
||||
xkb.active_layout().0 as u8
|
||||
});
|
||||
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.keyboard_layouts;
|
||||
|
||||
if state.keyboard_layouts.as_ref().unwrap().current_idx == idx {
|
||||
return;
|
||||
}
|
||||
|
||||
let event = Event::KeyboardLayoutSwitched { idx };
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
|
||||
pub fn ipc_refresh_layout(&mut self) {
|
||||
self.ipc_refresh_workspaces();
|
||||
self.ipc_refresh_windows();
|
||||
self.ipc_refresh_overview();
|
||||
}
|
||||
|
||||
fn ipc_refresh_workspaces(&mut self) {
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _span = tracy_client::span!("State::ipc_refresh_workspaces");
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.workspaces;
|
||||
|
||||
let mut events = Vec::new();
|
||||
let layout = &self.niri.layout;
|
||||
let focused_ws_id = layout.active_workspace().map(|ws| ws.id().get());
|
||||
|
||||
// Check for workspace changes.
|
||||
let mut seen = HashSet::new();
|
||||
let mut need_workspaces_changed = false;
|
||||
for (mon, ws_idx, ws) in layout.workspaces() {
|
||||
let id = ws.id().get();
|
||||
seen.insert(id);
|
||||
|
||||
let Some(ipc_ws) = state.workspaces.get(&id) else {
|
||||
// A new workspace was added.
|
||||
need_workspaces_changed = true;
|
||||
break;
|
||||
};
|
||||
|
||||
// Check for any changes that we can't signal as individual events.
|
||||
let output_name = mon.map(|mon| mon.output_name());
|
||||
if ipc_ws.idx != u8::try_from(ws_idx + 1).unwrap_or(u8::MAX)
|
||||
|| ipc_ws.name.as_ref() != ws.name()
|
||||
|| ipc_ws.output.as_ref() != output_name
|
||||
{
|
||||
need_workspaces_changed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
let active_window_id = ws.active_window().map(|win| win.id().get());
|
||||
if ipc_ws.active_window_id != active_window_id {
|
||||
events.push(Event::WorkspaceActiveWindowChanged {
|
||||
workspace_id: id,
|
||||
active_window_id,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if this workspace urgent state changed.
|
||||
let urgent = ws.is_urgent();
|
||||
if urgent != ipc_ws.is_urgent {
|
||||
events.push(Event::WorkspaceUrgencyChanged { id, urgent });
|
||||
}
|
||||
|
||||
// Check if this workspace became focused.
|
||||
let is_focused = Some(id) == focused_ws_id;
|
||||
if is_focused && !ipc_ws.is_focused {
|
||||
events.push(Event::WorkspaceActivated { id, focused: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this workspace became active.
|
||||
let is_active = mon.is_some_and(|mon| mon.active_workspace_idx() == ws_idx);
|
||||
if is_active && !ipc_ws.is_active {
|
||||
events.push(Event::WorkspaceActivated { id, focused: false });
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any workspaces were removed.
|
||||
if !need_workspaces_changed && state.workspaces.keys().any(|id| !seen.contains(id)) {
|
||||
need_workspaces_changed = true;
|
||||
}
|
||||
|
||||
if need_workspaces_changed {
|
||||
events.clear();
|
||||
|
||||
let workspaces = layout
|
||||
.workspaces()
|
||||
.map(|(mon, ws_idx, ws)| {
|
||||
let id = ws.id().get();
|
||||
Workspace {
|
||||
id,
|
||||
idx: u8::try_from(ws_idx + 1).unwrap_or(u8::MAX),
|
||||
name: ws.name().cloned(),
|
||||
output: mon.map(|mon| mon.output_name().clone()),
|
||||
is_urgent: ws.is_urgent(),
|
||||
is_active: mon.is_some_and(|mon| mon.active_workspace_idx() == ws_idx),
|
||||
is_focused: Some(id) == focused_ws_id,
|
||||
active_window_id: ws.active_window().map(|win| win.id().get()),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
events.push(Event::WorkspacesChanged { workspaces });
|
||||
}
|
||||
|
||||
for event in events {
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn ipc_refresh_windows(&mut self) {
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _span = tracy_client::span!("State::ipc_refresh_windows");
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.windows;
|
||||
|
||||
let mut events = Vec::new();
|
||||
let layout = &self.niri.layout;
|
||||
|
||||
// Check for window changes.
|
||||
let mut seen = HashSet::new();
|
||||
let mut focused_id = None;
|
||||
layout.with_windows(|mapped, _, ws_id| {
|
||||
let id = mapped.id().get();
|
||||
seen.insert(id);
|
||||
|
||||
if mapped.is_focused() {
|
||||
focused_id = Some(id);
|
||||
}
|
||||
|
||||
let Some(ipc_win) = state.windows.get(&id) else {
|
||||
let window = make_ipc_window(mapped, ws_id);
|
||||
events.push(Event::WindowOpenedOrChanged { window });
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace_id = ws_id.map(|id| id.get());
|
||||
let mut changed =
|
||||
ipc_win.workspace_id != workspace_id || ipc_win.is_floating != mapped.is_floating();
|
||||
|
||||
changed |= with_toplevel_role(mapped.toplevel(), |role| {
|
||||
ipc_win.title != role.title || ipc_win.app_id != role.app_id
|
||||
});
|
||||
|
||||
if changed {
|
||||
let window = make_ipc_window(mapped, ws_id);
|
||||
events.push(Event::WindowOpenedOrChanged { window });
|
||||
return;
|
||||
}
|
||||
|
||||
if mapped.is_focused() && !ipc_win.is_focused {
|
||||
events.push(Event::WindowFocusChanged { id: Some(id) });
|
||||
}
|
||||
|
||||
let urgent = mapped.is_urgent();
|
||||
if urgent != ipc_win.is_urgent {
|
||||
events.push(Event::WindowUrgencyChanged { id, urgent })
|
||||
}
|
||||
});
|
||||
|
||||
// Check for closed windows.
|
||||
let mut ipc_focused_id = None;
|
||||
for (id, ipc_win) in &state.windows {
|
||||
if !seen.contains(id) {
|
||||
events.push(Event::WindowClosed { id: *id });
|
||||
}
|
||||
|
||||
if ipc_win.is_focused {
|
||||
ipc_focused_id = Some(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Extra check for focus becoming None, since the checks above only work for focus becoming
|
||||
// a different window.
|
||||
if focused_id.is_none() && ipc_focused_id.is_some() {
|
||||
events.push(Event::WindowFocusChanged { id: None });
|
||||
}
|
||||
|
||||
for event in events {
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ipc_refresh_overview(&mut self) {
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.overview;
|
||||
let is_open = self.niri.layout.is_overview_open();
|
||||
|
||||
if state.is_open == is_open {
|
||||
return;
|
||||
}
|
||||
|
||||
let event = Event::OverviewOpenedOrClosed { is_open };
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
use niri_config::layer_rule::LayerRule;
|
||||
use niri_config::Config;
|
||||
use smithay::backend::renderer::element::surface::{
|
||||
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
|
||||
};
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::desktop::{LayerSurface, PopupManager};
|
||||
use smithay::utils::{Logical, Point, Scale, Size};
|
||||
use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer};
|
||||
|
||||
use super::ResolvedLayerRules;
|
||||
use crate::animation::Clock;
|
||||
use crate::layout::shadow::Shadow;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::shadow::ShadowRenderElement;
|
||||
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use crate::render_helpers::{RenderTarget, SplitElements};
|
||||
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MappedLayer {
|
||||
/// The surface itself.
|
||||
surface: LayerSurface,
|
||||
|
||||
/// Up-to-date rules.
|
||||
rules: ResolvedLayerRules,
|
||||
|
||||
/// Buffer to draw instead of the surface when it should be blocked out.
|
||||
block_out_buffer: SolidColorBuffer,
|
||||
|
||||
/// The shadow around the surface.
|
||||
shadow: Shadow,
|
||||
|
||||
/// The view size for the layer surface's output.
|
||||
view_size: Size<f64, Logical>,
|
||||
|
||||
/// Scale of the output the layer surface is on (and rounds its sizes to).
|
||||
scale: f64,
|
||||
|
||||
/// Clock for driving animations.
|
||||
clock: Clock,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
LayerSurfaceRenderElement<R> => {
|
||||
Wayland = WaylandSurfaceRenderElement<R>,
|
||||
SolidColor = SolidColorRenderElement,
|
||||
Shadow = ShadowRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
impl MappedLayer {
|
||||
pub fn new(
|
||||
surface: LayerSurface,
|
||||
rules: ResolvedLayerRules,
|
||||
view_size: Size<f64, Logical>,
|
||||
scale: f64,
|
||||
clock: Clock,
|
||||
config: &Config,
|
||||
) -> Self {
|
||||
let mut shadow_config = config.layout.shadow;
|
||||
// Shadows for layer surfaces need to be explicitly enabled.
|
||||
shadow_config.on = false;
|
||||
let shadow_config = rules.shadow.resolve_against(shadow_config);
|
||||
|
||||
Self {
|
||||
surface,
|
||||
rules,
|
||||
block_out_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
|
||||
view_size,
|
||||
scale,
|
||||
shadow: Shadow::new(shadow_config),
|
||||
clock,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: &Config) {
|
||||
let mut shadow_config = config.layout.shadow;
|
||||
// Shadows for layer surfaces need to be explicitly enabled.
|
||||
shadow_config.on = false;
|
||||
let shadow_config = self.rules.shadow.resolve_against(shadow_config);
|
||||
self.shadow.update_config(shadow_config);
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
self.shadow.update_shaders();
|
||||
}
|
||||
|
||||
pub fn update_sizes(&mut self, view_size: Size<f64, Logical>, scale: f64) {
|
||||
self.view_size = view_size;
|
||||
self.scale = scale;
|
||||
}
|
||||
|
||||
pub fn update_render_elements(&mut self, size: Size<f64, Logical>) {
|
||||
// Round to physical pixels.
|
||||
let size = size
|
||||
.to_physical_precise_round(self.scale)
|
||||
.to_logical(self.scale);
|
||||
|
||||
self.block_out_buffer.resize(size);
|
||||
|
||||
let radius = self.rules.geometry_corner_radius.unwrap_or_default();
|
||||
// FIXME: is_active based on keyboard focus?
|
||||
self.shadow
|
||||
.update_render_elements(size, true, radius, self.scale, 1.);
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
self.rules.baba_is_float
|
||||
}
|
||||
|
||||
pub fn surface(&self) -> &LayerSurface {
|
||||
&self.surface
|
||||
}
|
||||
|
||||
pub fn rules(&self) -> &ResolvedLayerRules {
|
||||
&self.rules
|
||||
}
|
||||
|
||||
/// Recomputes the resolved layer rules and returns whether they changed.
|
||||
pub fn recompute_layer_rules(&mut self, rules: &[LayerRule], is_at_startup: bool) -> bool {
|
||||
let new_rules = ResolvedLayerRules::compute(rules, &self.surface, is_at_startup);
|
||||
if new_rules == self.rules {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.rules = new_rules;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn place_within_backdrop(&self) -> bool {
|
||||
if !self.rules.place_within_backdrop {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.surface.layer() != Layer::Background {
|
||||
return false;
|
||||
}
|
||||
|
||||
let state = self.surface.cached_state();
|
||||
if state.exclusive_zone != ExclusiveZone::DontCare {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn bob_offset(&self) -> Point<f64, Logical> {
|
||||
if !self.rules.baba_is_float {
|
||||
return Point::from((0., 0.));
|
||||
}
|
||||
|
||||
let y = baba_is_float_offset(self.clock.now(), self.view_size.h);
|
||||
let y = round_logical_in_physical(self.scale, y);
|
||||
Point::from((0., y))
|
||||
}
|
||||
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<f64, Logical>,
|
||||
target: RenderTarget,
|
||||
) -> SplitElements<LayerSurfaceRenderElement<R>> {
|
||||
let mut rv = SplitElements::default();
|
||||
|
||||
let scale = Scale::from(self.scale);
|
||||
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
|
||||
let location = location + self.bob_offset();
|
||||
|
||||
if target.should_block_out(self.rules.block_out_from) {
|
||||
// Round to physical pixels.
|
||||
let location = location.to_physical_precise_round(scale).to_logical(scale);
|
||||
|
||||
// FIXME: take geometry-corner-radius into account.
|
||||
let elem = SolidColorRenderElement::from_buffer(
|
||||
&self.block_out_buffer,
|
||||
location,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
rv.normal.push(elem.into());
|
||||
} else {
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let buf_pos = location;
|
||||
|
||||
let surface = self.surface.wl_surface();
|
||||
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let offset = popup_offset - popup.geometry().loc;
|
||||
|
||||
rv.popups.extend(render_elements_from_surface_tree(
|
||||
renderer,
|
||||
popup.wl_surface(),
|
||||
(buf_pos + offset.to_f64()).to_physical_precise_round(scale),
|
||||
scale,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
));
|
||||
}
|
||||
|
||||
rv.normal = render_elements_from_surface_tree(
|
||||
renderer,
|
||||
surface,
|
||||
buf_pos.to_physical_precise_round(scale),
|
||||
scale,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
}
|
||||
|
||||
let location = location.to_physical_precise_round(scale).to_logical(scale);
|
||||
rv.normal
|
||||
.extend(self.shadow.render(renderer, location).map(Into::into));
|
||||
|
||||
rv
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use niri_config::layer_rule::{LayerRule, Match};
|
||||
use niri_config::{BlockOutFrom, CornerRadius, ShadowRule};
|
||||
use smithay::desktop::LayerSurface;
|
||||
|
||||
pub mod mapped;
|
||||
pub use mapped::MappedLayer;
|
||||
|
||||
/// Rules fully resolved for a layer-shell surface.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ResolvedLayerRules {
|
||||
/// Extra opacity to draw this layer surface with.
|
||||
pub opacity: Option<f32>,
|
||||
|
||||
/// Whether to block out this layer surface from certain render targets.
|
||||
pub block_out_from: Option<BlockOutFrom>,
|
||||
|
||||
/// Shadow overrides.
|
||||
pub shadow: ShadowRule,
|
||||
|
||||
/// Corner radius to assume this layer surface has.
|
||||
pub geometry_corner_radius: Option<CornerRadius>,
|
||||
|
||||
/// Whether to place this layer surface within the overview backdrop.
|
||||
pub place_within_backdrop: bool,
|
||||
|
||||
/// Whether to bob this window up and down.
|
||||
pub baba_is_float: bool,
|
||||
}
|
||||
|
||||
impl ResolvedLayerRules {
|
||||
pub const fn empty() -> Self {
|
||||
Self {
|
||||
opacity: None,
|
||||
block_out_from: None,
|
||||
shadow: ShadowRule {
|
||||
off: false,
|
||||
on: false,
|
||||
offset: None,
|
||||
softness: None,
|
||||
spread: None,
|
||||
draw_behind_window: None,
|
||||
color: None,
|
||||
inactive_color: None,
|
||||
},
|
||||
geometry_corner_radius: None,
|
||||
place_within_backdrop: false,
|
||||
baba_is_float: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute(rules: &[LayerRule], surface: &LayerSurface, is_at_startup: bool) -> Self {
|
||||
let _span = tracy_client::span!("ResolvedLayerRules::compute");
|
||||
|
||||
let mut resolved = ResolvedLayerRules::empty();
|
||||
|
||||
for rule in rules {
|
||||
let matches = |m: &Match| {
|
||||
if let Some(at_startup) = m.at_startup {
|
||||
if at_startup != is_at_startup {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
surface_matches(surface, m)
|
||||
};
|
||||
|
||||
if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if rule.excludes.iter().any(matches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(x) = rule.opacity {
|
||||
resolved.opacity = Some(x);
|
||||
}
|
||||
if let Some(x) = rule.block_out_from {
|
||||
resolved.block_out_from = Some(x);
|
||||
}
|
||||
if let Some(x) = rule.geometry_corner_radius {
|
||||
resolved.geometry_corner_radius = Some(x);
|
||||
}
|
||||
if let Some(x) = rule.place_within_backdrop {
|
||||
resolved.place_within_backdrop = x;
|
||||
}
|
||||
if let Some(x) = rule.baba_is_float {
|
||||
resolved.baba_is_float = x;
|
||||
}
|
||||
|
||||
resolved.shadow.merge_with(&rule.shadow);
|
||||
}
|
||||
|
||||
resolved
|
||||
}
|
||||
}
|
||||
|
||||
fn surface_matches(surface: &LayerSurface, m: &Match) -> bool {
|
||||
if let Some(namespace_re) = &m.namespace {
|
||||
if !namespace_re.0.is_match(surface.namespace()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
+121
-55
@@ -1,18 +1,17 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use glam::{Mat3, Vec2};
|
||||
use niri_config::BlockOutFrom;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::texture::TextureRenderElement;
|
||||
use smithay::backend::renderer::element::utils::{
|
||||
Relocate, RelocateRenderElement, RescaleRenderElement,
|
||||
};
|
||||
use smithay::backend::renderer::element::{Id, Kind, RenderElement};
|
||||
use smithay::backend::renderer::element::{Kind, RenderElement};
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform};
|
||||
use smithay::backend::renderer::{Renderer as _, Texture};
|
||||
use smithay::backend::renderer::Texture;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
|
||||
use smithay::wayland::compositor::{Blocker, BlockerState};
|
||||
|
||||
use crate::animation::Animation;
|
||||
use crate::niri_render_elements;
|
||||
@@ -20,39 +19,35 @@ use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||
use crate::render_helpers::shader_element::ShaderRenderElement;
|
||||
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
|
||||
use crate::render_helpers::snapshot::RenderSnapshot;
|
||||
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
|
||||
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
|
||||
use crate::utils::transaction::TransactionBlocker;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClosingWindow {
|
||||
/// Contents of the window.
|
||||
texture: GlesTexture,
|
||||
buffer: TextureBuffer<GlesTexture>,
|
||||
|
||||
/// Blocked-out contents of the window.
|
||||
blocked_out_texture: GlesTexture,
|
||||
|
||||
/// Scale that the textures was rendered with.
|
||||
texture_scale: Scale<f64>,
|
||||
|
||||
/// ID of the textures' renderer.
|
||||
texture_renderer_id: usize,
|
||||
blocked_out_buffer: TextureBuffer<GlesTexture>,
|
||||
|
||||
/// Where the window should be blocked out from.
|
||||
block_out_from: Option<BlockOutFrom>,
|
||||
|
||||
/// Size of the window geometry.
|
||||
geo_size: Size<i32, Logical>,
|
||||
geo_size: Size<f64, Logical>,
|
||||
|
||||
/// Position in the workspace.
|
||||
pos: Point<i32, Logical>,
|
||||
pos: Point<f64, Logical>,
|
||||
|
||||
/// How much the texture should be offset.
|
||||
texture_offset: Point<f64, Logical>,
|
||||
buffer_offset: Point<f64, Logical>,
|
||||
|
||||
/// How much the blocked-out texture should be offset.
|
||||
blocked_out_texture_offset: Point<f64, Logical>,
|
||||
blocked_out_buffer_offset: Point<f64, Logical>,
|
||||
|
||||
/// The closing animation.
|
||||
anim: Animation,
|
||||
anim_state: AnimationState,
|
||||
|
||||
/// Random seed for the shader.
|
||||
random_seed: f32,
|
||||
@@ -65,13 +60,37 @@ niri_render_elements! {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AnimationState {
|
||||
Waiting {
|
||||
/// Blocker for a transaction before starting the animation.
|
||||
blocker: TransactionBlocker,
|
||||
anim: Animation,
|
||||
},
|
||||
Animating(Animation),
|
||||
}
|
||||
|
||||
impl AnimationState {
|
||||
pub fn new(blocker: TransactionBlocker, anim: Animation) -> Self {
|
||||
if blocker.state() == BlockerState::Pending {
|
||||
Self::Waiting { blocker, anim }
|
||||
} else {
|
||||
// This actually doesn't normally happen because the window is removed only after the
|
||||
// closing animation is created. Though, it does happen with disable-transactions debug
|
||||
// flag.
|
||||
Self::Animating(anim)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClosingWindow {
|
||||
pub fn new<E: RenderElement<GlesRenderer>>(
|
||||
renderer: &mut GlesRenderer,
|
||||
snapshot: RenderSnapshot<E, E>,
|
||||
scale: Scale<f64>,
|
||||
geo_size: Size<i32, Logical>,
|
||||
pos: Point<i32, Logical>,
|
||||
geo_size: Size<f64, Logical>,
|
||||
pos: Point<f64, Logical>,
|
||||
blocker: TransactionBlocker,
|
||||
anim: Animation,
|
||||
) -> anyhow::Result<Self> {
|
||||
let _span = tracy_client::span!("ClosingWindow::new");
|
||||
@@ -86,69 +105,120 @@ impl ClosingWindow {
|
||||
)
|
||||
.context("error rendering to texture")?;
|
||||
|
||||
let buffer = TextureBuffer::from_texture(
|
||||
renderer,
|
||||
texture,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
let offset = geo.loc.to_f64().to_logical(scale);
|
||||
|
||||
Ok((texture, offset))
|
||||
Ok((buffer, offset))
|
||||
};
|
||||
|
||||
let (texture, texture_offset) =
|
||||
let (buffer, buffer_offset) =
|
||||
render_to_texture(snapshot.contents).context("error rendering contents")?;
|
||||
let (blocked_out_texture, blocked_out_texture_offset) =
|
||||
let (blocked_out_buffer, blocked_out_buffer_offset) =
|
||||
render_to_texture(snapshot.blocked_out_contents)
|
||||
.context("error rendering blocked-out contents")?;
|
||||
|
||||
Ok(Self {
|
||||
texture,
|
||||
blocked_out_texture,
|
||||
texture_scale: scale,
|
||||
texture_renderer_id: renderer.id(),
|
||||
buffer,
|
||||
blocked_out_buffer,
|
||||
block_out_from: snapshot.block_out_from,
|
||||
geo_size,
|
||||
pos,
|
||||
texture_offset,
|
||||
blocked_out_texture_offset,
|
||||
anim,
|
||||
buffer_offset,
|
||||
blocked_out_buffer_offset,
|
||||
anim_state: AnimationState::new(blocker, anim),
|
||||
random_seed: fastrand::f32(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, current_time: Duration) {
|
||||
self.anim.set_current_time(current_time);
|
||||
pub fn advance_animations(&mut self) {
|
||||
match &mut self.anim_state {
|
||||
AnimationState::Waiting { blocker, anim } => {
|
||||
if blocker.state() != BlockerState::Pending {
|
||||
let anim = anim.restarted(0., 1., 0.);
|
||||
self.anim_state = AnimationState::Animating(anim);
|
||||
}
|
||||
}
|
||||
AnimationState::Animating(_anim) => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
!self.anim.is_done()
|
||||
match &self.anim_state {
|
||||
AnimationState::Waiting { .. } => true,
|
||||
AnimationState::Animating(anim) => !anim.is_done(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut GlesRenderer,
|
||||
view_rect: Rectangle<i32, Logical>,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
target: RenderTarget,
|
||||
) -> ClosingWindowRenderElement {
|
||||
let progress = self.anim.value();
|
||||
let clamped_progress = self.anim.clamped_value().clamp(0., 1.);
|
||||
|
||||
let (texture, offset) = if target.should_block_out(self.block_out_from) {
|
||||
(&self.blocked_out_texture, self.blocked_out_texture_offset)
|
||||
let (buffer, offset) = if target.should_block_out(self.block_out_from) {
|
||||
(&self.blocked_out_buffer, self.blocked_out_buffer_offset)
|
||||
} else {
|
||||
(&self.texture, self.texture_offset)
|
||||
(&self.buffer, self.buffer_offset)
|
||||
};
|
||||
|
||||
let anim = match &self.anim_state {
|
||||
AnimationState::Waiting { .. } => {
|
||||
let elem = TextureRenderElement::from_texture_buffer(
|
||||
buffer.clone(),
|
||||
Point::from((0., 0.)),
|
||||
1.,
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
|
||||
let elem = PrimaryGpuTextureRenderElement(elem);
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), 1.);
|
||||
|
||||
let mut location = self.pos + offset;
|
||||
location.x -= view_rect.loc.x;
|
||||
let elem = RelocateRenderElement::from_element(
|
||||
elem,
|
||||
location.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
);
|
||||
|
||||
return elem.into();
|
||||
}
|
||||
AnimationState::Animating(anim) => anim,
|
||||
};
|
||||
|
||||
let progress = anim.value();
|
||||
let clamped_progress = anim.clamped_value().clamp(0., 1.);
|
||||
|
||||
if Shaders::get(renderer).program(ProgramType::Close).is_some() {
|
||||
let area_loc = Vec2::new(view_rect.loc.x as f32, view_rect.loc.y as f32);
|
||||
let area_size = Vec2::new(view_rect.size.w as f32, view_rect.size.h as f32);
|
||||
|
||||
let geo_loc = Vec2::new(self.pos.x as f32, self.pos.y as f32);
|
||||
// Round to physical pixels relative to the view position. This is similar to what
|
||||
// happens when rendering normal windows.
|
||||
let relative = self.pos - view_rect.loc;
|
||||
let pos = view_rect.loc + relative.to_physical_precise_round(scale).to_logical(scale);
|
||||
|
||||
let geo_loc = Vec2::new(pos.x as f32, pos.y as f32);
|
||||
let geo_size = Vec2::new(self.geo_size.w as f32, self.geo_size.h as f32);
|
||||
|
||||
let input_to_geo = Mat3::from_scale(area_size / geo_size)
|
||||
* Mat3::from_translation((area_loc - geo_loc) / area_size);
|
||||
|
||||
let tex_scale = Vec2::new(self.texture_scale.x as f32, self.texture_scale.y as f32);
|
||||
let tex_scale = self.buffer.texture_scale();
|
||||
let tex_scale = Vec2::new(tex_scale.x as f32, tex_scale.y as f32);
|
||||
let tex_loc = Vec2::new(offset.x as f32, offset.y as f32);
|
||||
let tex_size = Vec2::new(texture.width() as f32, texture.height() as f32) / tex_scale;
|
||||
let tex_size = self.buffer.texture().size();
|
||||
let tex_size = Vec2::new(tex_size.w as f32, tex_size.h as f32) / tex_scale;
|
||||
|
||||
let geo_to_tex =
|
||||
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
|
||||
@@ -157,6 +227,7 @@ impl ClosingWindow {
|
||||
ProgramType::Close,
|
||||
view_rect.size,
|
||||
None,
|
||||
scale.x as f32,
|
||||
1.,
|
||||
vec![
|
||||
mat3_uniform("niri_input_to_geo", input_to_geo),
|
||||
@@ -166,22 +237,17 @@ impl ClosingWindow {
|
||||
Uniform::new("niri_clamped_progress", clamped_progress as f32),
|
||||
Uniform::new("niri_random_seed", self.random_seed),
|
||||
],
|
||||
HashMap::from([(String::from("niri_tex"), texture.clone())]),
|
||||
HashMap::from([(String::from("niri_tex"), buffer.texture().clone())]),
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.with_location(Point::from((0, 0)))
|
||||
.with_location(Point::from((0., 0.)))
|
||||
.into();
|
||||
}
|
||||
|
||||
let elem = TextureRenderElement::from_static_texture(
|
||||
Id::new(),
|
||||
self.texture_renderer_id,
|
||||
let elem = TextureRenderElement::from_texture_buffer(
|
||||
buffer.clone(),
|
||||
Point::from((0., 0.)),
|
||||
texture.clone(),
|
||||
self.texture_scale.x as i32,
|
||||
Transform::Normal,
|
||||
Some(1. - clamped_progress as f32),
|
||||
None,
|
||||
1. - clamped_progress as f32,
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
@@ -189,15 +255,15 @@ impl ClosingWindow {
|
||||
|
||||
let elem = PrimaryGpuTextureRenderElement(elem);
|
||||
|
||||
let center = self.geo_size.to_point().to_f64().downscale(2.);
|
||||
let center = self.geo_size.to_point().downscale(2.);
|
||||
let elem = RescaleRenderElement::from_element(
|
||||
elem,
|
||||
(center - offset).to_physical_precise_round(scale),
|
||||
((1. - clamped_progress) / 5. + 0.8).max(0.),
|
||||
);
|
||||
|
||||
let mut location = self.pos.to_f64() + offset;
|
||||
location.x -= view_rect.loc.x as f64;
|
||||
let mut location = self.pos + offset;
|
||||
location.x -= view_rect.loc.x;
|
||||
let elem = RelocateRenderElement::from_element(
|
||||
elem,
|
||||
location.to_physical_precise_round(scale),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+70
-58
@@ -1,23 +1,22 @@
|
||||
use std::cmp::{max, min};
|
||||
use std::iter::zip;
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
use niri_config::{CornerRadius, Gradient, GradientRelativeTo};
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
||||
use smithay::backend::renderer::element::{Element as _, Kind};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::border::BorderRenderElement;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FocusRing {
|
||||
buffers: [SolidColorBuffer; 8],
|
||||
locations: [Point<i32, Logical>; 8],
|
||||
sizes: [Size<i32, Logical>; 8],
|
||||
locations: [Point<f64, Logical>; 8],
|
||||
sizes: [Size<f64, Logical>; 8],
|
||||
borders: [BorderRenderElement; 8],
|
||||
full_size: Size<i32, Logical>,
|
||||
full_size: Size<f64, Logical>,
|
||||
is_border: bool,
|
||||
use_border_shader: bool,
|
||||
config: niri_config::FocusRing,
|
||||
@@ -54,30 +53,38 @@ impl FocusRing {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn update_render_elements(
|
||||
&mut self,
|
||||
win_size: Size<i32, Logical>,
|
||||
win_size: Size<f64, Logical>,
|
||||
is_active: bool,
|
||||
is_border: bool,
|
||||
view_rect: Rectangle<i32, Logical>,
|
||||
is_urgent: bool,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
radius: CornerRadius,
|
||||
scale: f64,
|
||||
alpha: f32,
|
||||
) {
|
||||
let width = i32::from(self.config.width);
|
||||
self.full_size = win_size + Size::from((width * 2, width * 2));
|
||||
let width = self.config.width.0;
|
||||
self.full_size = win_size + Size::from((width, width)).upscale(2.);
|
||||
|
||||
let color = if is_active {
|
||||
let color = if is_urgent {
|
||||
self.config.urgent_color
|
||||
} else if is_active {
|
||||
self.config.active_color
|
||||
} else {
|
||||
self.config.inactive_color
|
||||
};
|
||||
|
||||
for buf in &mut self.buffers {
|
||||
buf.set_color(color.into());
|
||||
buf.set_color(color.to_array_premul());
|
||||
}
|
||||
|
||||
let radius = radius.fit_to(self.full_size.w as f32, self.full_size.h as f32);
|
||||
|
||||
let gradient = if is_active {
|
||||
let gradient = if is_urgent {
|
||||
self.config.urgent_gradient
|
||||
} else if is_active {
|
||||
self.config.active_gradient
|
||||
} else {
|
||||
self.config.inactive_gradient
|
||||
@@ -86,14 +93,9 @@ impl FocusRing {
|
||||
self.use_border_shader = radius != CornerRadius::default() || gradient.is_some();
|
||||
|
||||
// Set the defaults for solid color + rounded corners.
|
||||
let gradient = gradient.unwrap_or(Gradient {
|
||||
from: color,
|
||||
to: color,
|
||||
angle: 0,
|
||||
relative_to: GradientRelativeTo::Window,
|
||||
});
|
||||
let gradient = gradient.unwrap_or_else(|| Gradient::from(color));
|
||||
|
||||
let full_rect = Rectangle::from_loc_and_size((-width, -width), self.full_size);
|
||||
let full_rect = Rectangle::new(Point::from((-width, -width)), self.full_size);
|
||||
let gradient_area = match gradient.relative_to {
|
||||
GradientRelativeTo::Window => full_rect,
|
||||
GradientRelativeTo::WorkspaceView => view_rect,
|
||||
@@ -107,39 +109,48 @@ impl FocusRing {
|
||||
0.
|
||||
};
|
||||
|
||||
let ceil = |logical: f64| (logical * scale).ceil() / scale;
|
||||
|
||||
// All of this stuff should end up aligned to physical pixels because:
|
||||
// * Window size and border width are rounded to physical pixels before being passed to this
|
||||
// function.
|
||||
// * We will ceil the corner radii below.
|
||||
// * We do not divide anything, only add, subtract and multiply by integers.
|
||||
// * At rendering time, tile positions are rounded to physical pixels.
|
||||
|
||||
if is_border {
|
||||
let top_left = max(width, radius.top_left.ceil() as i32);
|
||||
let top_right = min(
|
||||
let top_left = f64::max(width, ceil(f64::from(radius.top_left)));
|
||||
let top_right = f64::min(
|
||||
self.full_size.w - top_left,
|
||||
max(width, radius.top_right.ceil() as i32),
|
||||
f64::max(width, ceil(f64::from(radius.top_right))),
|
||||
);
|
||||
let bottom_left = min(
|
||||
let bottom_left = f64::min(
|
||||
self.full_size.h - top_left,
|
||||
max(width, radius.bottom_left.ceil() as i32),
|
||||
f64::max(width, ceil(f64::from(radius.bottom_left))),
|
||||
);
|
||||
let bottom_right = min(
|
||||
let bottom_right = f64::min(
|
||||
self.full_size.h - top_right,
|
||||
min(
|
||||
f64::min(
|
||||
self.full_size.w - bottom_left,
|
||||
max(width, radius.bottom_right.ceil() as i32),
|
||||
f64::max(width, ceil(f64::from(radius.bottom_right))),
|
||||
),
|
||||
);
|
||||
|
||||
// Top edge.
|
||||
self.sizes[0] = Size::from((win_size.w + width * 2 - top_left - top_right, width));
|
||||
self.sizes[0] = Size::from((win_size.w + width * 2. - top_left - top_right, width));
|
||||
self.locations[0] = Point::from((-width + top_left, -width));
|
||||
|
||||
// Bottom edge.
|
||||
self.sizes[1] =
|
||||
Size::from((win_size.w + width * 2 - bottom_left - bottom_right, width));
|
||||
Size::from((win_size.w + width * 2. - bottom_left - bottom_right, width));
|
||||
self.locations[1] = Point::from((-width + bottom_left, win_size.h));
|
||||
|
||||
// Left edge.
|
||||
self.sizes[2] = Size::from((width, win_size.h + width * 2 - top_left - bottom_left));
|
||||
self.sizes[2] = Size::from((width, win_size.h + width * 2. - top_left - bottom_left));
|
||||
self.locations[2] = Point::from((-width, -width + top_left));
|
||||
|
||||
// Right edge.
|
||||
self.sizes[3] = Size::from((width, win_size.h + width * 2 - top_right - bottom_right));
|
||||
self.sizes[3] = Size::from((width, win_size.h + width * 2. - top_right - bottom_right));
|
||||
self.locations[3] = Point::from((win_size.w, -width + top_right));
|
||||
|
||||
// Top-left corner.
|
||||
@@ -168,13 +179,16 @@ impl FocusRing {
|
||||
for (border, (loc, size)) in zip(&mut self.borders, zip(self.locations, self.sizes)) {
|
||||
border.update(
|
||||
size,
|
||||
Rectangle::from_loc_and_size(gradient_area.loc - loc, gradient_area.size),
|
||||
gradient.from.into(),
|
||||
gradient.to.into(),
|
||||
Rectangle::new(gradient_area.loc - loc, gradient_area.size),
|
||||
gradient.in_,
|
||||
gradient.from,
|
||||
gradient.to,
|
||||
((gradient.angle as f32) - 90.).to_radians(),
|
||||
Rectangle::from_loc_and_size(full_rect.loc - loc, full_rect.size),
|
||||
Rectangle::new(full_rect.loc - loc, full_rect.size),
|
||||
rounded_corner_border_width,
|
||||
radius,
|
||||
scale as f32,
|
||||
alpha,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -184,16 +198,16 @@ impl FocusRing {
|
||||
|
||||
self.borders[0].update(
|
||||
self.sizes[0],
|
||||
Rectangle::from_loc_and_size(
|
||||
gradient_area.loc - self.locations[0],
|
||||
gradient_area.size,
|
||||
),
|
||||
gradient.from.into(),
|
||||
gradient.to.into(),
|
||||
Rectangle::new(gradient_area.loc - self.locations[0], gradient_area.size),
|
||||
gradient.in_,
|
||||
gradient.from,
|
||||
gradient.to,
|
||||
((gradient.angle as f32) - 90.).to_radians(),
|
||||
Rectangle::from_loc_and_size(full_rect.loc - self.locations[0], full_rect.size),
|
||||
Rectangle::new(full_rect.loc - self.locations[0], full_rect.size),
|
||||
rounded_corner_border_width,
|
||||
radius,
|
||||
scale as f32,
|
||||
alpha,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -203,8 +217,7 @@ impl FocusRing {
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<i32, Logical>,
|
||||
scale: Scale<f64>,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
let mut rv = ArrayVec::<_, 8>::new();
|
||||
|
||||
@@ -215,24 +228,19 @@ impl FocusRing {
|
||||
let border_width = -self.locations[0].y;
|
||||
|
||||
// If drawing as a border with width = 0, then there's nothing to draw.
|
||||
if self.is_border && border_width == 0 {
|
||||
if self.is_border && border_width == 0. {
|
||||
return rv.into_iter();
|
||||
}
|
||||
|
||||
let has_border_shader = BorderRenderElement::has_shader(renderer);
|
||||
|
||||
let mut push = |buffer, border: &BorderRenderElement, location: Point<i32, Logical>| {
|
||||
let mut push = |buffer, border: &BorderRenderElement, location: Point<f64, Logical>| {
|
||||
let elem = if self.use_border_shader && has_border_shader {
|
||||
border.clone().with_location(location).into()
|
||||
} else {
|
||||
SolidColorRenderElement::from_buffer(
|
||||
buffer,
|
||||
location.to_physical_precise_round(scale),
|
||||
scale,
|
||||
1.,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into()
|
||||
let alpha = border.alpha();
|
||||
SolidColorRenderElement::from_buffer(buffer, location, alpha, Kind::Unspecified)
|
||||
.into()
|
||||
};
|
||||
rv.push(elem);
|
||||
};
|
||||
@@ -252,11 +260,15 @@ impl FocusRing {
|
||||
rv.into_iter()
|
||||
}
|
||||
|
||||
pub fn width(&self) -> i32 {
|
||||
self.config.width.into()
|
||||
pub fn width(&self) -> f64 {
|
||||
self.config.width.0
|
||||
}
|
||||
|
||||
pub fn is_off(&self) -> bool {
|
||||
self.config.off
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &niri_config::FocusRing {
|
||||
&self.config
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
use niri_config::{CornerRadius, FloatOrInt};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
|
||||
use super::focus_ring::{FocusRing, FocusRingRenderElement};
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InsertHintElement {
|
||||
inner: FocusRing,
|
||||
}
|
||||
|
||||
pub type InsertHintRenderElement = FocusRingRenderElement;
|
||||
|
||||
impl InsertHintElement {
|
||||
pub fn new(config: niri_config::InsertHint) -> Self {
|
||||
Self {
|
||||
inner: FocusRing::new(niri_config::FocusRing {
|
||||
off: config.off,
|
||||
width: FloatOrInt(0.),
|
||||
active_color: config.color,
|
||||
inactive_color: config.color,
|
||||
urgent_color: config.color,
|
||||
active_gradient: config.gradient,
|
||||
inactive_gradient: config.gradient,
|
||||
urgent_gradient: config.gradient,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: niri_config::InsertHint) {
|
||||
self.inner.update_config(niri_config::FocusRing {
|
||||
off: config.off,
|
||||
width: FloatOrInt(0.),
|
||||
active_color: config.color,
|
||||
inactive_color: config.color,
|
||||
urgent_color: config.color,
|
||||
active_gradient: config.gradient,
|
||||
inactive_gradient: config.gradient,
|
||||
urgent_gradient: config.gradient,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
self.inner.update_shaders();
|
||||
}
|
||||
|
||||
pub fn update_render_elements(
|
||||
&mut self,
|
||||
size: Size<f64, Logical>,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
radius: CornerRadius,
|
||||
scale: f64,
|
||||
) {
|
||||
self.inner
|
||||
.update_render_elements(size, true, false, false, view_rect, radius, scale, 1.);
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
self.inner.render(renderer, location)
|
||||
}
|
||||
}
|
||||
+3649
-2240
File diff suppressed because it is too large
Load Diff
+1395
-523
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,18 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use glam::{Mat3, Vec2};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::texture::TextureRenderElement;
|
||||
use smithay::backend::renderer::element::utils::{
|
||||
Relocate, RelocateRenderElement, RescaleRenderElement,
|
||||
};
|
||||
use smithay::backend::renderer::element::{Id, Kind, RenderElement};
|
||||
use smithay::backend::renderer::element::{Element as _, Kind, RenderElement};
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, Uniform};
|
||||
use smithay::backend::renderer::{Renderer as _, Texture};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
|
||||
use smithay::backend::renderer::Texture;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
||||
|
||||
use crate::animation::Animation;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||
use crate::render_helpers::render_to_encompassing_texture;
|
||||
use crate::render_helpers::offscreen::{OffscreenBuffer, OffscreenData, OffscreenRenderElement};
|
||||
use crate::render_helpers::shader_element::ShaderRenderElement;
|
||||
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
|
||||
|
||||
@@ -24,11 +20,12 @@ use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
|
||||
pub struct OpenAnimation {
|
||||
anim: Animation,
|
||||
random_seed: f32,
|
||||
buffer: OffscreenBuffer,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
OpeningWindowRenderElement => {
|
||||
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
|
||||
Offscreen = RelocateRenderElement<RescaleRenderElement<OffscreenRenderElement>>,
|
||||
Shader = ShaderRenderElement,
|
||||
}
|
||||
}
|
||||
@@ -38,12 +35,11 @@ impl OpenAnimation {
|
||||
Self {
|
||||
anim,
|
||||
random_seed: fastrand::f32(),
|
||||
buffer: OffscreenBuffer::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, current_time: Duration) {
|
||||
self.anim.set_current_time(current_time);
|
||||
}
|
||||
pub fn advance_animations(&mut self) {}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.anim.is_done()
|
||||
@@ -55,37 +51,37 @@ impl OpenAnimation {
|
||||
&self,
|
||||
renderer: &mut GlesRenderer,
|
||||
elements: &[impl RenderElement<GlesRenderer>],
|
||||
geo_size: Size<i32, Logical>,
|
||||
location: Point<i32, Logical>,
|
||||
geo_size: Size<f64, Logical>,
|
||||
location: Point<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
) -> anyhow::Result<OpeningWindowRenderElement> {
|
||||
alpha: f32,
|
||||
) -> anyhow::Result<(OpeningWindowRenderElement, OffscreenData)> {
|
||||
let progress = self.anim.value();
|
||||
let clamped_progress = self.anim.clamped_value().clamp(0., 1.);
|
||||
|
||||
let (texture, _sync_point, geo) = render_to_encompassing_texture(
|
||||
renderer,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
Fourcc::Abgr8888,
|
||||
elements,
|
||||
)
|
||||
.context("error rendering to texture")?;
|
||||
|
||||
let offset = geo.loc.to_f64().to_logical(scale);
|
||||
let texture_size = geo.size.to_f64().to_logical(scale);
|
||||
let (elem, _sync_point, mut data) = self
|
||||
.buffer
|
||||
.render(renderer, scale, elements)
|
||||
.context("error rendering to offscreen buffer")?;
|
||||
|
||||
if Shaders::get(renderer).program(ProgramType::Open).is_some() {
|
||||
let mut area = Rectangle::from_loc_and_size(location.to_f64() + offset, texture_size);
|
||||
// OffscreenBuffer renders with Transform::Normal and the scale that we passed, so we
|
||||
// can assume that below.
|
||||
let offset = elem.offset();
|
||||
let texture = elem.texture();
|
||||
let texture_size = elem.logical_size();
|
||||
|
||||
let mut area = Rectangle::new(location + offset, texture_size);
|
||||
|
||||
// Expand the area a bit to allow for more varied effects.
|
||||
let mut target_size = area.size.upscale(1.5);
|
||||
target_size.w = f64::max(area.size.w + 1000., target_size.w);
|
||||
target_size.h = f64::max(area.size.h + 1000., target_size.h);
|
||||
let diff = target_size.to_point() - area.size.to_point();
|
||||
area.loc -= diff.downscale(2.);
|
||||
area.size += diff.to_size();
|
||||
let diff = (target_size.to_point() - area.size.to_point()).downscale(2.);
|
||||
let diff = diff.to_physical_precise_round(scale).to_logical(scale);
|
||||
area.loc -= diff;
|
||||
area.size += diff.upscale(2.).to_size();
|
||||
|
||||
let area = area.to_i32_up();
|
||||
let area_loc = Vec2::new(area.loc.x as f32, area.loc.y as f32);
|
||||
let area_size = Vec2::new(area.size.w as f32, area.size.h as f32);
|
||||
|
||||
@@ -102,11 +98,12 @@ impl OpenAnimation {
|
||||
let geo_to_tex =
|
||||
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
|
||||
|
||||
return Ok(ShaderRenderElement::new(
|
||||
let elem = ShaderRenderElement::new(
|
||||
ProgramType::Open,
|
||||
area.size,
|
||||
None,
|
||||
1.,
|
||||
scale.x as f32,
|
||||
alpha,
|
||||
vec![
|
||||
mat3_uniform("niri_input_to_geo", input_to_geo),
|
||||
Uniform::new("niri_geo_size", geo_size.to_array()),
|
||||
@@ -118,39 +115,29 @@ impl OpenAnimation {
|
||||
HashMap::from([(String::from("niri_tex"), texture.clone())]),
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.with_location(area.loc)
|
||||
.into());
|
||||
.with_location(area.loc);
|
||||
|
||||
// We're drawing the shader, not the offscreen itself.
|
||||
data.id = elem.id().clone();
|
||||
|
||||
return Ok((elem.into(), data));
|
||||
}
|
||||
|
||||
let elem = TextureRenderElement::from_static_texture(
|
||||
Id::new(),
|
||||
renderer.id(),
|
||||
Point::from((0., 0.)),
|
||||
texture.clone(),
|
||||
scale.x as i32,
|
||||
Transform::Normal,
|
||||
Some(clamped_progress as f32),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
let elem = elem.with_alpha(clamped_progress as f32 * alpha);
|
||||
|
||||
let elem = PrimaryGpuTextureRenderElement(elem);
|
||||
|
||||
let center = geo_size.to_point().to_f64().downscale(2.);
|
||||
let center = geo_size.to_point().downscale(2.);
|
||||
let elem = RescaleRenderElement::from_element(
|
||||
elem,
|
||||
(center - offset).to_physical_precise_round(scale),
|
||||
center.to_physical_precise_round(scale),
|
||||
(progress / 2. + 0.5).max(0.),
|
||||
);
|
||||
|
||||
let elem = RelocateRenderElement::from_element(
|
||||
elem,
|
||||
(location.to_f64() + offset).to_physical_precise_round(scale),
|
||||
location.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
);
|
||||
|
||||
Ok(elem.into())
|
||||
Ok((elem.into(), data))
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,184 @@
|
||||
use std::iter::zip;
|
||||
|
||||
use niri_config::CornerRadius;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::shadow::ShadowRenderElement;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Shadow {
|
||||
shader_rects: Vec<Rectangle<f64, Logical>>,
|
||||
shaders: Vec<ShadowRenderElement>,
|
||||
config: niri_config::Shadow,
|
||||
}
|
||||
|
||||
impl Shadow {
|
||||
pub fn new(config: niri_config::Shadow) -> Self {
|
||||
Self {
|
||||
shader_rects: Vec::new(),
|
||||
shaders: Vec::new(),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: niri_config::Shadow) {
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
for elem in &mut self.shaders {
|
||||
elem.damage_all();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_render_elements(
|
||||
&mut self,
|
||||
win_size: Size<f64, Logical>,
|
||||
is_active: bool,
|
||||
radius: CornerRadius,
|
||||
scale: f64,
|
||||
alpha: f32,
|
||||
) {
|
||||
let ceil = |logical: f64| (logical * scale).ceil() / scale;
|
||||
|
||||
// All of this stuff should end up aligned to physical pixels because:
|
||||
// * Window size is rounded to physical pixels before being passed to this function.
|
||||
// * We will ceil the corner radii below.
|
||||
// * We do not divide anything, only add, subtract and multiply by integers.
|
||||
// * At rendering time, tile positions are rounded to physical pixels.
|
||||
|
||||
let width = self.config.softness.0;
|
||||
// Like in CSS box-shadow.
|
||||
let sigma = width / 2.;
|
||||
// Adjust width to draw all necessary pixels.
|
||||
let width = ceil(sigma * 3.);
|
||||
|
||||
let offset = self.config.offset;
|
||||
let offset = Point::from((ceil(offset.x.0), ceil(offset.y.0)));
|
||||
|
||||
let spread = self.config.spread.0;
|
||||
let spread = ceil(spread.abs()).copysign(spread);
|
||||
let offset = offset - Point::from((spread, spread));
|
||||
|
||||
let win_radius = radius.fit_to(win_size.w as f32, win_size.h as f32);
|
||||
|
||||
let box_size = if spread >= 0. {
|
||||
win_size + Size::from((spread, spread)).upscale(2.)
|
||||
} else {
|
||||
// This is a saturating sub.
|
||||
win_size - Size::from((-spread, -spread)).upscale(2.)
|
||||
};
|
||||
let radius = win_radius.expanded_by(spread as f32);
|
||||
|
||||
let shader_size = box_size + Size::from((width, width)).upscale(2.);
|
||||
|
||||
let color = if is_active {
|
||||
self.config.color
|
||||
} else {
|
||||
// Default to slightly more transparent.
|
||||
self.config
|
||||
.inactive_color
|
||||
.unwrap_or(self.config.color * 0.75)
|
||||
};
|
||||
|
||||
let shader_geo = Rectangle::new(Point::from((-width, -width)), shader_size);
|
||||
|
||||
// This is actually offset relative to shader_geo, this is handled below.
|
||||
let window_geo = Rectangle::new(Point::from((0., 0.)), win_size);
|
||||
|
||||
if !self.config.draw_behind_window {
|
||||
let top_left = ceil(f64::from(win_radius.top_left));
|
||||
let top_right = f64::min(win_size.w - top_left, ceil(f64::from(win_radius.top_right)));
|
||||
let bottom_left = f64::min(
|
||||
win_size.h - top_left,
|
||||
ceil(f64::from(win_radius.bottom_left)),
|
||||
);
|
||||
let bottom_right = f64::min(
|
||||
win_size.h - top_right,
|
||||
f64::min(
|
||||
win_size.w - bottom_left,
|
||||
ceil(f64::from(win_radius.bottom_right)),
|
||||
),
|
||||
);
|
||||
|
||||
let top_left = Rectangle::new(Point::from((0., 0.)), Size::from((top_left, top_left)));
|
||||
let top_right = Rectangle::new(
|
||||
Point::from((win_size.w - top_right, 0.)),
|
||||
Size::from((top_right, top_right)),
|
||||
);
|
||||
let bottom_right = Rectangle::new(
|
||||
Point::from((win_size.w - bottom_right, win_size.h - bottom_right)),
|
||||
Size::from((bottom_right, bottom_right)),
|
||||
);
|
||||
let bottom_left = Rectangle::new(
|
||||
Point::from((0., win_size.h - bottom_left)),
|
||||
Size::from((bottom_left, bottom_left)),
|
||||
);
|
||||
|
||||
let mut background =
|
||||
window_geo.subtract_rects([top_left, top_right, bottom_right, bottom_left]);
|
||||
for rect in &mut background {
|
||||
rect.loc -= offset;
|
||||
}
|
||||
|
||||
self.shader_rects = shader_geo.subtract_rects(background);
|
||||
self.shaders
|
||||
.resize_with(self.shader_rects.len(), Default::default);
|
||||
|
||||
for (shader, rect) in zip(&mut self.shaders, &mut self.shader_rects) {
|
||||
shader.update(
|
||||
rect.size,
|
||||
Rectangle::new(rect.loc.upscale(-1.), box_size),
|
||||
color,
|
||||
sigma as f32,
|
||||
radius,
|
||||
scale as f32,
|
||||
Rectangle::new(window_geo.loc - offset - rect.loc, window_geo.size),
|
||||
win_radius,
|
||||
alpha,
|
||||
);
|
||||
|
||||
rect.loc += offset;
|
||||
}
|
||||
} else {
|
||||
self.shader_rects.resize_with(1, Default::default);
|
||||
self.shader_rects[0] = shader_geo;
|
||||
|
||||
self.shaders.resize_with(1, Default::default);
|
||||
self.shaders[0].update(
|
||||
shader_geo.size,
|
||||
Rectangle::new(shader_geo.loc.upscale(-1.), box_size),
|
||||
color,
|
||||
sigma as f32,
|
||||
radius,
|
||||
scale as f32,
|
||||
Rectangle::zero(),
|
||||
Default::default(),
|
||||
alpha,
|
||||
);
|
||||
|
||||
self.shader_rects[0].loc += offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
|
||||
if !self.config.on {
|
||||
return None.into_iter().flatten();
|
||||
}
|
||||
|
||||
let has_shadow_shader = ShadowRenderElement::has_shader(renderer);
|
||||
if !has_shadow_shader {
|
||||
return None.into_iter().flatten();
|
||||
}
|
||||
|
||||
let rv = zip(&self.shaders, &self.shader_rects)
|
||||
.map(move |(shader, rect)| shader.clone().with_location(location + rect.loc));
|
||||
|
||||
Some(rv).into_iter().flatten()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
use std::iter::zip;
|
||||
use std::mem;
|
||||
|
||||
use niri_config::{CornerRadius, Gradient, GradientRelativeTo, TabIndicatorPosition};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
|
||||
use super::tile::Tile;
|
||||
use super::LayoutElement;
|
||||
use crate::animation::{Animation, Clock};
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::border::BorderRenderElement;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::utils::{
|
||||
floor_logical_in_physical_max1, round_logical_in_physical, round_logical_in_physical_max1,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TabIndicator {
|
||||
shader_locs: Vec<Point<f64, Logical>>,
|
||||
shaders: Vec<BorderRenderElement>,
|
||||
open_anim: Option<Animation>,
|
||||
config: niri_config::TabIndicator,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TabInfo {
|
||||
/// Gradient for the tab indicator.
|
||||
pub gradient: Gradient,
|
||||
/// Tab geometry in the same coordinate system as the area.
|
||||
pub geometry: Rectangle<f64, Logical>,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
TabIndicatorRenderElement => {
|
||||
Gradient = BorderRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
impl TabIndicator {
|
||||
pub fn new(config: niri_config::TabIndicator) -> Self {
|
||||
Self {
|
||||
shader_locs: Vec::new(),
|
||||
shaders: Vec::new(),
|
||||
open_anim: None,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: niri_config::TabIndicator) {
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
for elem in &mut self.shaders {
|
||||
elem.damage_all();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self) {
|
||||
if let Some(anim) = &mut self.open_anim {
|
||||
if anim.is_done() {
|
||||
self.open_anim = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
self.open_anim.is_some()
|
||||
}
|
||||
|
||||
pub fn start_open_animation(&mut self, clock: Clock, config: niri_config::Animation) {
|
||||
self.open_anim = Some(Animation::new(clock, 0., 1., 0., config));
|
||||
}
|
||||
|
||||
fn tab_rects(
|
||||
&self,
|
||||
area: Rectangle<f64, Logical>,
|
||||
count: usize,
|
||||
scale: f64,
|
||||
) -> impl Iterator<Item = Rectangle<f64, Logical>> {
|
||||
let round = |logical: f64| round_logical_in_physical(scale, logical);
|
||||
let round_max1 = |logical: f64| round_logical_in_physical_max1(scale, logical);
|
||||
|
||||
let progress = self.open_anim.as_ref().map_or(1., |a| a.value().max(0.));
|
||||
|
||||
let width = round_max1(self.config.width.0);
|
||||
let gap = self.config.gap.0;
|
||||
let gap = round_max1(gap.abs()).copysign(gap);
|
||||
let gaps_between = round_max1(self.config.gaps_between_tabs.0);
|
||||
|
||||
let position = self.config.position;
|
||||
let side = match position {
|
||||
TabIndicatorPosition::Left | TabIndicatorPosition::Right => area.size.h,
|
||||
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => area.size.w,
|
||||
};
|
||||
let total_prop = self.config.length.total_proportion.unwrap_or(0.5);
|
||||
let min_length = round(side * total_prop.clamp(0., 2.));
|
||||
|
||||
// Compute px_per_tab before applying the animation to gaps_between in order to avoid it
|
||||
// growing and shrinking over the duration of the animation.
|
||||
let pixel = 1. / scale;
|
||||
let shortest_length = count as f64 * (pixel + gaps_between) - gaps_between;
|
||||
let length = f64::max(min_length, shortest_length);
|
||||
let px_per_tab = (length + gaps_between) / count as f64 - gaps_between;
|
||||
|
||||
let px_per_tab = px_per_tab * progress;
|
||||
let gaps_between = round(self.config.gaps_between_tabs.0 * progress);
|
||||
|
||||
let length = count as f64 * (px_per_tab + gaps_between) - gaps_between;
|
||||
let px_per_tab = floor_logical_in_physical_max1(scale, px_per_tab);
|
||||
let floored_length = count as f64 * (px_per_tab + gaps_between) - gaps_between;
|
||||
let mut ones_left = ((length - floored_length) / pixel).round() as usize;
|
||||
|
||||
let mut shader_loc = Point::from((-gap - width, round((side - length) / 2.)));
|
||||
match position {
|
||||
TabIndicatorPosition::Left => (),
|
||||
TabIndicatorPosition::Right => shader_loc.x = area.size.w + gap,
|
||||
TabIndicatorPosition::Top => mem::swap(&mut shader_loc.x, &mut shader_loc.y),
|
||||
TabIndicatorPosition::Bottom => {
|
||||
shader_loc.x = shader_loc.y;
|
||||
shader_loc.y = area.size.h + gap;
|
||||
}
|
||||
}
|
||||
shader_loc += area.loc;
|
||||
|
||||
(0..count).map(move |_| {
|
||||
let mut px_per_tab = px_per_tab;
|
||||
if ones_left > 0 {
|
||||
ones_left -= 1;
|
||||
px_per_tab += pixel;
|
||||
}
|
||||
|
||||
let loc = shader_loc;
|
||||
|
||||
match position {
|
||||
TabIndicatorPosition::Left | TabIndicatorPosition::Right => {
|
||||
shader_loc.y += px_per_tab + gaps_between
|
||||
}
|
||||
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => {
|
||||
shader_loc.x += px_per_tab + gaps_between
|
||||
}
|
||||
}
|
||||
|
||||
let size = match position {
|
||||
TabIndicatorPosition::Left | TabIndicatorPosition::Right => {
|
||||
Size::from((width, px_per_tab))
|
||||
}
|
||||
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => {
|
||||
Size::from((px_per_tab, width))
|
||||
}
|
||||
};
|
||||
|
||||
Rectangle::new(loc, size)
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn update_render_elements(
|
||||
&mut self,
|
||||
enabled: bool,
|
||||
// Geometry of the tabs area.
|
||||
area: Rectangle<f64, Logical>,
|
||||
// View rect relative to the tabs area.
|
||||
area_view_rect: Rectangle<f64, Logical>,
|
||||
// Tab count, should match the tabs iterator length.
|
||||
tab_count: usize,
|
||||
tabs: impl Iterator<Item = TabInfo>,
|
||||
is_active: bool,
|
||||
scale: f64,
|
||||
) {
|
||||
if !enabled || self.config.off {
|
||||
self.shader_locs.clear();
|
||||
self.shaders.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
let count = tab_count;
|
||||
if self.config.hide_when_single_tab && count == 1 {
|
||||
self.shader_locs.clear();
|
||||
self.shaders.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
self.shaders.resize_with(count, Default::default);
|
||||
self.shader_locs.resize_with(count, Default::default);
|
||||
|
||||
let position = self.config.position;
|
||||
let radius = self.config.corner_radius.0 as f32;
|
||||
let shared_rounded_corners = self.config.gaps_between_tabs.0 == 0.;
|
||||
let mut tabs_left = tab_count;
|
||||
|
||||
let rects = self.tab_rects(area, count, scale);
|
||||
for ((shader, loc), (tab, rect)) in zip(
|
||||
zip(&mut self.shaders, &mut self.shader_locs),
|
||||
zip(tabs, rects),
|
||||
) {
|
||||
*loc = rect.loc;
|
||||
|
||||
let mut gradient_area = match tab.gradient.relative_to {
|
||||
GradientRelativeTo::Window => tab.geometry,
|
||||
GradientRelativeTo::WorkspaceView => area_view_rect,
|
||||
};
|
||||
gradient_area.loc -= *loc;
|
||||
|
||||
let mut color_from = tab.gradient.from;
|
||||
let mut color_to = tab.gradient.to;
|
||||
if !is_active {
|
||||
color_from *= 0.5;
|
||||
color_to *= 0.5;
|
||||
}
|
||||
|
||||
let radius = if shared_rounded_corners && tab_count > 1 {
|
||||
if tabs_left == tab_count {
|
||||
// First tab.
|
||||
match position {
|
||||
TabIndicatorPosition::Left | TabIndicatorPosition::Right => CornerRadius {
|
||||
top_left: radius,
|
||||
top_right: radius,
|
||||
bottom_right: 0.,
|
||||
bottom_left: 0.,
|
||||
},
|
||||
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => CornerRadius {
|
||||
top_left: radius,
|
||||
top_right: 0.,
|
||||
bottom_right: 0.,
|
||||
bottom_left: radius,
|
||||
},
|
||||
}
|
||||
} else if tabs_left == 1 {
|
||||
// Last tab.
|
||||
match position {
|
||||
TabIndicatorPosition::Left | TabIndicatorPosition::Right => CornerRadius {
|
||||
top_left: 0.,
|
||||
top_right: 0.,
|
||||
bottom_right: radius,
|
||||
bottom_left: radius,
|
||||
},
|
||||
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => CornerRadius {
|
||||
top_left: 0.,
|
||||
top_right: radius,
|
||||
bottom_right: radius,
|
||||
bottom_left: 0.,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Tab in the middle.
|
||||
CornerRadius::default()
|
||||
}
|
||||
} else {
|
||||
// Separate tabs, or the only tab.
|
||||
CornerRadius::from(radius)
|
||||
};
|
||||
let radius = radius.fit_to(rect.size.w as f32, rect.size.h as f32);
|
||||
tabs_left -= 1;
|
||||
|
||||
shader.update(
|
||||
rect.size,
|
||||
gradient_area,
|
||||
tab.gradient.in_,
|
||||
color_from,
|
||||
color_to,
|
||||
((tab.gradient.angle as f32) - 90.).to_radians(),
|
||||
Rectangle::from_size(rect.size),
|
||||
0.,
|
||||
radius,
|
||||
scale as f32,
|
||||
1.,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hit(
|
||||
&self,
|
||||
area: Rectangle<f64, Logical>,
|
||||
tab_count: usize,
|
||||
scale: f64,
|
||||
point: Point<f64, Logical>,
|
||||
) -> Option<usize> {
|
||||
if self.config.off {
|
||||
return None;
|
||||
}
|
||||
|
||||
let count = tab_count;
|
||||
if self.config.hide_when_single_tab && count == 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.tab_rects(area, count, scale)
|
||||
.enumerate()
|
||||
.find_map(|(idx, rect)| rect.contains(point).then_some(idx))
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
pos: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = TabIndicatorRenderElement> + '_ {
|
||||
let has_border_shader = BorderRenderElement::has_shader(renderer);
|
||||
if !has_border_shader {
|
||||
return None.into_iter().flatten();
|
||||
}
|
||||
|
||||
let rv = zip(&self.shaders, &self.shader_locs)
|
||||
.map(move |(shader, loc)| shader.clone().with_location(pos + *loc))
|
||||
.map(TabIndicatorRenderElement::from);
|
||||
|
||||
Some(rv).into_iter().flatten()
|
||||
}
|
||||
|
||||
/// Extra size occupied by the tab indicator.
|
||||
pub fn extra_size(&self, tab_count: usize, scale: f64) -> Size<f64, Logical> {
|
||||
if self.config.off
|
||||
|| !self.config.place_within_column
|
||||
|| (self.config.hide_when_single_tab && tab_count == 1)
|
||||
{
|
||||
return Size::from((0., 0.));
|
||||
}
|
||||
|
||||
let round = |logical: f64| round_logical_in_physical(scale, logical);
|
||||
let width = round(self.config.width.0);
|
||||
let gap = round(self.config.gap.0);
|
||||
|
||||
// No, I am *not* falling into the rabbit hole of "what if the tab indicator is wide enough
|
||||
// that it peeks from the other side of the window".
|
||||
let size = f64::max(0., width + gap);
|
||||
|
||||
match self.config.position {
|
||||
TabIndicatorPosition::Left | TabIndicatorPosition::Right => Size::from((size, 0.)),
|
||||
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => Size::from((0., size)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Offset of the tabbed content due to space occupied by the tab indicator.
|
||||
pub fn content_offset(&self, tab_count: usize, scale: f64) -> Point<f64, Logical> {
|
||||
match self.config.position {
|
||||
TabIndicatorPosition::Left | TabIndicatorPosition::Top => {
|
||||
self.extra_size(tab_count, scale).to_point()
|
||||
}
|
||||
TabIndicatorPosition::Right | TabIndicatorPosition::Bottom => Point::from((0., 0.)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config(&self) -> niri_config::TabIndicator {
|
||||
self.config
|
||||
}
|
||||
}
|
||||
|
||||
impl TabInfo {
|
||||
pub fn from_tile<W: LayoutElement>(
|
||||
tile: &Tile<W>,
|
||||
position: Point<f64, Logical>,
|
||||
is_active: bool,
|
||||
is_urgent: bool,
|
||||
config: &niri_config::TabIndicator,
|
||||
) -> Self {
|
||||
let rules = tile.window().rules();
|
||||
let rule = rules.tab_indicator;
|
||||
|
||||
let gradient_from_rule = || {
|
||||
let (color, gradient) = if is_urgent {
|
||||
(rule.urgent_color, rule.urgent_gradient)
|
||||
} else if is_active {
|
||||
(rule.active_color, rule.active_gradient)
|
||||
} else {
|
||||
(rule.inactive_color, rule.inactive_gradient)
|
||||
};
|
||||
let color = color.map(Gradient::from);
|
||||
gradient.or(color)
|
||||
};
|
||||
|
||||
let gradient_from_config = || {
|
||||
let (color, gradient) = if is_urgent {
|
||||
(config.urgent_color, config.urgent_gradient)
|
||||
} else if is_active {
|
||||
(config.active_color, config.active_gradient)
|
||||
} else {
|
||||
(config.inactive_color, config.inactive_gradient)
|
||||
};
|
||||
let color = color.map(Gradient::from);
|
||||
gradient.or(color)
|
||||
};
|
||||
|
||||
let gradient_from_border = || {
|
||||
// Come up with tab indicator gradient matching the focus ring or the border, whichever
|
||||
// one is enabled.
|
||||
let focus_ring_config = tile.focus_ring().config();
|
||||
let border_config = tile.border().config();
|
||||
let config = if focus_ring_config.off {
|
||||
border_config
|
||||
} else {
|
||||
focus_ring_config
|
||||
};
|
||||
|
||||
let (color, gradient) = if is_urgent {
|
||||
(config.urgent_color, config.urgent_gradient)
|
||||
} else if is_active {
|
||||
(config.active_color, config.active_gradient)
|
||||
} else {
|
||||
(config.inactive_color, config.inactive_gradient)
|
||||
};
|
||||
gradient.unwrap_or_else(|| Gradient::from(color))
|
||||
};
|
||||
|
||||
let gradient = gradient_from_rule()
|
||||
.or_else(gradient_from_config)
|
||||
.unwrap_or_else(gradient_from_border);
|
||||
|
||||
let geometry = Rectangle::new(position, tile.animated_tile_size());
|
||||
|
||||
TabInfo { gradient, geometry }
|
||||
}
|
||||
}
|
||||
+3666
File diff suppressed because it is too large
Load Diff
+519
-207
File diff suppressed because it is too large
Load Diff
+1443
-3006
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user