mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
Compare commits
787 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| d96a66ddff | |||
| bfaf9ae060 | |||
| 2da0aaace8 | |||
| ee12bbc9ed | |||
| cc4026f588 | |||
| aa74120143 | |||
| 473ef22de2 | |||
| d76b213e03 | |||
| 4dc7a6ceb8 | |||
| 36d3e70f11 | |||
| a2f74c9bff | |||
| 0ce08b598c | |||
| ae63773737 | |||
| c5ca412829 | |||
| cbfc682f9a | |||
| c64d9e5223 | |||
| 4e31f7e047 | |||
| 109d99fe82 | |||
| eb9bbe3352 | |||
| 229ca90507 | |||
| 17a71bd424 | |||
| a39aaa312d | |||
| 3f802d0193 | |||
| df36eac25b | |||
| 609b1a02d0 | |||
| 5335ef454b | |||
| 496cd59df9 | |||
| 3e385d5c48 | |||
| b87fba2182 | |||
| 3d63f5e644 | |||
| 1096f0cf0e | |||
| 78978219a0 | |||
| 5999ba6a5e | |||
| 94a9b48a0f | |||
| d776ab7763 | |||
| 5f40221051 | |||
| b14405904a | |||
| e06776c5d4 | |||
| 55e550262d | |||
| e5ccc9332c | |||
| 36a54615ca | |||
| 9004c83954 | |||
| 29c7552852 | |||
| d2ed42a157 | |||
| 4073f9f522 | |||
| 464441f9eb | |||
| bc29256b9d | |||
| beba87354a | |||
| 078724369d | |||
| 75393faca3 | |||
| 22cdd044d3 | |||
| 719270854a | |||
| 8900960e76 | |||
| 47a8e75fd5 | |||
| 6d9cfe2882 | |||
| de0ad85711 | |||
| f091e64b12 | |||
| e454cd6282 | |||
| 1c14a0a2a9 | |||
| 2fd9a03bd7 | |||
| b101f9b5f8 | |||
| 34bcc6ea93 | |||
| 9dfa121b8e | |||
| c4ebb9f58e | |||
| 38e329aab9 | |||
| 95a1a01fdc | |||
| c61940c40e | |||
| ed2b6d3894 | |||
| 47925948a3 | |||
| 5248e53499 | |||
| 9847a652af | |||
| 96823eea38 | |||
| ea59091869 | |||
| 2e4a2e13b1 | |||
| df0ee996ee | |||
| 65b9c74f62 | |||
| 2dff674470 | |||
| 23850e1c60 | |||
| 641b44e006 | |||
| 1394afaae9 | |||
| 314ad9d3e5 | |||
| 99eb1227b1 | |||
| 79093baeee | |||
| 7093385b4d | |||
| 3748f6cd6a | |||
| 73cc0079d6 | |||
| 69aeba2a4d | |||
| 7aab413048 | |||
| 74996a2416 | |||
| 8ab50f9d1c | |||
| 5c32031111 | |||
| 85680a57da | |||
| 1a8d6b1f1d | |||
| 185f294200 | |||
| c6d64dae7a | |||
| 5dddc850fc | |||
| 2f42f8ac75 | |||
| 42cef79c69 | |||
| d86df5025c | |||
| 9309b3be61 | |||
| c5be2dd549 | |||
| 365dbacae7 | |||
| af9caa1d9b | |||
| 68ff36f683 | |||
| c0d5001e90 | |||
| f3ded0c2e6 | |||
| f43fa55526 | |||
| c1c43c5393 | |||
| 5899010c96 | |||
| 9f3715b731 | |||
| 8d99e3c015 | |||
| 9df71bcb5d | |||
| 04c5b9ad74 | |||
| fd6c8c7790 | |||
| 3e598c565e | |||
| e261b641ed | |||
| dc1d2b706c | |||
| f9b008163c | |||
| 279659ac90 | |||
| c2d03d82ce | |||
| 5299590290 | |||
| 1681ed16d9 | |||
| d4bed70884 | |||
| 49f5402669 | |||
| 2ecbb3f6f8 | |||
| 6a80078259 | |||
| 303c51ee20 | |||
| 37a836f462 | |||
| 361ede4bcd | |||
| 4fc80124ad | |||
| ba44aeda4a | |||
| b5f7e4bd83 | |||
| b98b95883d | |||
| 568c35ff87 | |||
| c4f600bded | |||
| 2c8d1030ab | |||
| f51dd67f2d | |||
| 3509de6fbf | |||
| 0477986a0d | |||
| 914237fa11 | |||
| 0b93c46ce8 | |||
| 0fcd981b86 | |||
| 5c4153e26b | |||
| 4d010b7943 | |||
| 65c342f2cb | |||
| 47f6c85f64 | |||
| 3b37f1a557 | |||
| dee0abb713 | |||
| bbb4a64126 | |||
| dfe49aa705 | |||
| 7ca39baf9e | |||
| 73e9ef5fe2 | |||
| c40d4f3268 | |||
| 1b496ee21f | |||
| bde46dab52 | |||
| 21ef5aded8 | |||
| b288102866 | |||
| ff42f9b9d3 | |||
| c163e58167 | |||
| a9094b43d4 | |||
| 9e33320b11 | |||
| c40de5364d | |||
| 69f723d68a | |||
| 568fbe26fe | |||
| f8412ecff3 | |||
| 3c6d8062c5 | |||
| 40374942db | |||
| 2c873044e8 | |||
| 1336a581a6 | |||
| 8b0dc1902c | |||
| 9d5f1c7ef7 | |||
| 71be19b234 | |||
| 4fd9300bdb | |||
| 2bb6dd8c48 | |||
| 7319f37f7a | |||
| 0cd149c939 | |||
| 5383a0591f | |||
| 0c68609063 | |||
| 6cd3f96a10 | |||
| 1888696567 | |||
| b9e789619f | |||
| dd011f1012 | |||
| 301a2c0661 | |||
| 956bf7c0a8 | |||
| 209492e700 | |||
| 7e0d3d31f7 | |||
| e448cfb0ef | |||
| 6aceb3a798 | |||
| 4856522a7a | |||
| c1432bfa96 | |||
| ec0531264e | |||
| 03fc439150 | |||
| 83aec41df3 | |||
| 8be9381974 | |||
| dc56f9885c | |||
| 2b3a80b477 | |||
| 294f16f76c | |||
| 4f56ff16f9 | |||
| fe79a6a4e2 | |||
| 950fcf6328 |
@@ -14,6 +14,9 @@ assignees: ''
|
||||
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
|
||||
* niri version:
|
||||
|
||||
<!-- Write your distribution, e.g. Fedora 40 Silverblue -->
|
||||
* Distro:
|
||||
|
||||
<!-- Write your GPU vendor and model, e.g. AMD RX 6700M -->
|
||||
* GPU:
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
smithay:
|
||||
patterns:
|
||||
- "smithay"
|
||||
- "smithay-drm-extras"
|
||||
rust-dependencies:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
ignore:
|
||||
- dependency-name: "Andrew-Chen-Wang/github-wiki-action"
|
||||
+50
-31
@@ -23,8 +23,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 +32,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 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
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -63,7 +62,7 @@ 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
|
||||
@@ -74,8 +73,7 @@ jobs:
|
||||
fail-fast: false
|
||||
|
||||
name: visual tests
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -84,8 +82,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 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
|
||||
|
||||
@@ -98,9 +96,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: 'msrv - 1.72.0'
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
name: 'msrv - 1.80.1'
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -109,10 +106,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 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@1.72.0
|
||||
- uses: dtolnay/rust-toolchain@1.80.1
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
@@ -123,8 +120,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 +129,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 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
|
||||
with:
|
||||
@@ -146,25 +142,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 +168,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 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 libdisplay-info-devel 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,7 +188,7 @@ jobs:
|
||||
uses: DeterminateSystems/nix-installer-action@v3
|
||||
continue-on-error: true
|
||||
|
||||
- run: nix build
|
||||
- run: nix flake check
|
||||
continue-on-error: true
|
||||
|
||||
publish-wiki:
|
||||
@@ -202,10 +196,35 @@ jobs:
|
||||
needs: build
|
||||
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
|
||||
|
||||
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
+1726
-1252
File diff suppressed because it is too large
Load Diff
+76
-38
@@ -1,22 +1,29 @@
|
||||
[workspace]
|
||||
members = ["niri-visual-tests"]
|
||||
members = [
|
||||
"niri-config",
|
||||
"niri-ipc",
|
||||
"niri-visual-tests",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.4"
|
||||
version = "25.1.0"
|
||||
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"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.81"
|
||||
bitflags = "2.5.0"
|
||||
clap = { version = "~4.4.18", features = ["derive"] }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
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.95"
|
||||
bitflags = "2.7.0"
|
||||
clap = { version = "4.5.26", features = ["derive"] }
|
||||
insta = "1.42.0"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
serde_json = "1.0.135"
|
||||
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]
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
@@ -35,46 +42,53 @@ 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"
|
||||
async-channel = { version = "2.2.0", optional = true }
|
||||
async-io = { version = "1.13.0", optional = true }
|
||||
arrayvec = "0.7.6"
|
||||
async-channel = "2.3.1"
|
||||
async-io = { version = "2.4.0", optional = true }
|
||||
atomic = "0.6.0"
|
||||
bitflags.workspace = true
|
||||
bytemuck = { version = "1.15.0", features = ["derive"] }
|
||||
calloop = { version = "0.13.0", features = ["executor", "futures-io"] }
|
||||
bytemuck = { version = "1.21.0", features = ["derive"] }
|
||||
calloop = { version = "0.14.2", features = ["executor", "futures-io"] }
|
||||
clap = { workspace = true, features = ["string"] }
|
||||
directories = "5.0.1"
|
||||
drm-ffi = "0.7.1"
|
||||
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
|
||||
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.29.2"
|
||||
input = { version = "0.9.1", features = ["libinput_1_21"] }
|
||||
keyframe = { version = "1.1.1", default-features = false }
|
||||
libc = "0.2.153"
|
||||
log = { version = "0.4.21", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
niri-config = { version = "0.1.4", path = "niri-config" }
|
||||
niri-ipc = { version = "0.1.4", 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.169"
|
||||
libdisplay-info = "0.2.2"
|
||||
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
niri-config = { version = "25.1.0", path = "niri-config" }
|
||||
niri-ipc = { version = "25.1.0", path = "niri-ipc", features = ["clap"] }
|
||||
ordered-float = "4.6.0"
|
||||
pango = { version = "0.20.7", features = ["v1_44"] }
|
||||
pangocairo = "0.20.7"
|
||||
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
|
||||
png = "0.17.16"
|
||||
portable-atomic = { version = "1.10.0", default-features = false, features = ["float"] }
|
||||
profiling = "1.0.16"
|
||||
sd-notify = "0.4.3"
|
||||
serde.workspace = true
|
||||
serde_json = "1.0.115"
|
||||
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.7"
|
||||
wayland-scanner = "0.31.5"
|
||||
xcursor = "0.3.8"
|
||||
zbus = { version = "5.2.0", optional = true }
|
||||
|
||||
[dependencies.smithay]
|
||||
workspace = true
|
||||
@@ -95,20 +109,29 @@ features = [
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1.4.0"
|
||||
proptest-derive = "0.4.0"
|
||||
xshell = "0.2.5"
|
||||
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.7"
|
||||
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-channel", "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 = []
|
||||
|
||||
@@ -121,8 +144,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.4"
|
||||
version = "25.01"
|
||||
assets = [
|
||||
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
||||
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
||||
@@ -134,3 +161,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,12 +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/discussions/325">Setup Showcase</a>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
## About
|
||||
|
||||
@@ -24,26 +28,51 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
|
||||
|
||||
## Features
|
||||
|
||||
- Scrollable tiling
|
||||
- Built from the ground up for scrollable tiling
|
||||
- Dynamic workspaces like in GNOME
|
||||
- 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
|
||||
- [Touchpad gestures](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515)
|
||||
- [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
|
||||
- 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
|
||||
|
||||
## Video Demo
|
||||
|
||||
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
|
||||
https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
|
||||
|
||||
## Status
|
||||
|
||||
A lot of the essential functionality is implemented, plus some goodies on top.
|
||||
Feel free to give niri a try.
|
||||
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 might have rendering 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) will make X11 apps look blurry; this needs to be supported in xwayland-satellite.
|
||||
For games, you can run them in [gamescope] at native resolution, even with display scaling.
|
||||
|
||||
## Inspiration
|
||||
|
||||
@@ -52,147 +81,28 @@ 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.
|
||||
|
||||
## Packages
|
||||
## Tile Scrollably Elsewhere
|
||||
|
||||
There are several community-maintained distribution packages that you can use to install niri.
|
||||
Here are some of them:
|
||||
Here are some other projects which implement a similar workflow:
|
||||
|
||||
- Fedora COPR (I maintain this one myself): https://copr.fedorainfracloud.org/coprs/yalter/niri/
|
||||
- AUR: [niri](https://aur.archlinux.org/packages/niri), [niri-bin](https://aur.archlinux.org/packages/niri-bin), [niri-git](https://aur.archlinux.org/packages/niri-git)
|
||||
- NixOS Flake: https://github.com/sodiboo/niri-flake
|
||||
- FreeBSD Ports: https://www.freshports.org/x11-wm/niri
|
||||
- Gentoo GURU: https://gpo.zugaina.org/Overlays/guru/gui-wm/niri
|
||||
|
||||
## Building
|
||||
|
||||
First, install the dependencies for your distribution.
|
||||
|
||||
- Ubuntu 23.10:
|
||||
|
||||
```sh
|
||||
sudo apt-get install -y 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
|
||||
```
|
||||
|
||||
- Fedora:
|
||||
|
||||
```sh
|
||||
sudo dnf install 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
|
||||
```
|
||||
|
||||
Next, get latest stable Rust: https://rustup.rs/
|
||||
|
||||
Then, build niri with `cargo build --release`.
|
||||
|
||||
### NixOS/Nix
|
||||
|
||||
We have a community-maintained flake which provides a devshell with required dependencies. Use `nix build` to build niri, and then run `./results/bin/niri`.
|
||||
|
||||
If you're not on NixOS, you may need [NixGL](https://github.com/nix-community/nixGL) to run the resulting binary:
|
||||
|
||||
```
|
||||
nix run --impure github:guibou/nixGL -- ./results/bin/niri
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
The recommended way to install and run niri is as a standalone desktop session.
|
||||
To do that, put files into the correct directories according to this table.
|
||||
|
||||
| File | Destination |
|
||||
| ---- | ----------- |
|
||||
| `target/release/niri` | `/usr/bin/` |
|
||||
| `resources/niri-session` | `/usr/bin/` |
|
||||
| `resources/niri.desktop` | `/usr/share/wayland-sessions/` |
|
||||
| `resources/niri-portals.conf` | `/usr/share/xdg-desktop-portal/` |
|
||||
| `resources/niri.service` | `/usr/lib/systemd/user/` |
|
||||
| `resources/niri-shutdown.target` | `/usr/lib/systemd/user/` |
|
||||
|
||||
Doing this will make niri appear in GDM and, presumably, other display managers.
|
||||
|
||||
## Running
|
||||
|
||||
`cargo run --release`
|
||||
|
||||
Inside an existing desktop session, it will run in a window.
|
||||
On a TTY, it will run natively.
|
||||
|
||||
To exit when running on a TTY, press <kbd>Super</kbd><kbd>Shift</kbd><kbd>E</kbd>.
|
||||
|
||||
### Session
|
||||
|
||||
If you followed the recommended installation steps above, niri should appear in your display manager.
|
||||
Starting it from there will run niri as a desktop session.
|
||||
|
||||
The niri session will autostart apps through the systemd xdg-autostart target.
|
||||
You can also autostart systemd services like [mako] by symlinking them into `$HOME/.config/systemd/user/niri.service.wants/`.
|
||||
A step-by-step process for this is explained [on the wiki](https://github.com/YaLTeR/niri/wiki/Example-systemd-Setup).
|
||||
|
||||
Niri also works with some parts of xdg-desktop-portal-gnome.
|
||||
In particular, it supports file choosers and monitor screencasting (e.g. to [OBS]).
|
||||
|
||||
[This wiki page](https://github.com/YaLTeR/niri/wiki/Important-Software) explains how to run important software required for normal desktop use, including portals.
|
||||
|
||||
## Configuration
|
||||
|
||||
Please check [this wiki page](https://github.com/YaLTeR/niri/wiki/Configuration:-Overview) for an overview of niri configuration.
|
||||
It also links to wiki pages containing thorough documentation for all options with examples.
|
||||
|
||||
## Default Hotkeys
|
||||
|
||||
When running on a TTY, the Mod key is <kbd>Super</kbd>.
|
||||
When running in a window, the Mod key is <kbd>Alt</kbd>.
|
||||
|
||||
The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kbd> will move the focused window or column there.
|
||||
|
||||
| Hotkey | Description |
|
||||
| ------ | ----------- |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>/</kbd> | Show a list of important niri hotkeys |
|
||||
| <kbd>Mod</kbd><kbd>T</kbd> | Spawn `alacritty` (terminal) |
|
||||
| <kbd>Mod</kbd><kbd>D</kbd> | Spawn `fuzzel` (application launcher) |
|
||||
| <kbd>Mod</kbd><kbd>Alt</kbd><kbd>L</kbd> | Spawn `swaylock` (screen locker) |
|
||||
| <kbd>Mod</kbd><kbd>Q</kbd> | Close the focused window |
|
||||
| <kbd>Mod</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd>←</kbd> | Focus the column to the left |
|
||||
| <kbd>Mod</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>→</kbd> | Focus the column to the right |
|
||||
| <kbd>Mod</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>↓</kbd> | Focus the window below in a column |
|
||||
| <kbd>Mod</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>↑</kbd> | Focus the window above in a column |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>←</kbd> | Move the focused column to the left |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>→</kbd> | Move the focused column to the right |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↓</kbd> | Move the focused window below in a column |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↑</kbd> | Move the focused window above in a column |
|
||||
| <kbd>Mod</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>End</kbd> | Focus the first or the last column |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>End</kbd> | Move the focused column to the very start or to the very end |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Focus the monitor to the side |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Move the focused column to the monitor to the side |
|
||||
| <kbd>Mod</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>PageDown</kbd> | Switch to the workspace below |
|
||||
| <kbd>Mod</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>PageUp</kbd> | Switch to the workspace above |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageDown</kbd> | Move the focused column to the workspace below |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused column to the workspace above |
|
||||
| <kbd>Mod</kbd><kbd>1</kbd>–<kbd>9</kbd> | Switch to a workspace by index |
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>1</kbd>–<kbd>9</kbd> | Move the focused column to a workspace by index |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageDown</kbd> | Move the focused workspace down |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageUp</kbd> | Move the focused workspace up |
|
||||
| <kbd>Mod</kbd><kbd>,</kbd> | Consume the window to the right into the focused column |
|
||||
| <kbd>Mod</kbd><kbd>.</kbd> | Expel the focused window into its own column |
|
||||
| <kbd>Mod</kbd><kbd>R</kbd> | Toggle between preset column widths |
|
||||
| <kbd>Mod</kbd><kbd>F</kbd> | Maximize column |
|
||||
| <kbd>Mod</kbd><kbd>C</kbd> | Center column within view |
|
||||
| <kbd>Mod</kbd><kbd>-</kbd> | Decrease column width by 10% |
|
||||
| <kbd>Mod</kbd><kbd>=</kbd> | Increase column width by 10% |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>-</kbd> | Decrease window height by 10% |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>=</kbd> | Increase window height by 10% |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>F</kbd> | Toggle full-screen on the focused window |
|
||||
| <kbd>PrtSc</kbd> | Take an area screenshot. Select the area to screenshot with mouse, then press Space to save the screenshot, or Escape to cancel |
|
||||
| <kbd>Alt</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused window to clipboard and to `~/Pictures/Screenshots/` |
|
||||
| <kbd>Ctrl</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused monitor to clipboard and to `~/Pictures/Screenshots/` |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>E</kbd> | Exit niri |
|
||||
- [PaperWM]: scrollable tiling on top of GNOME Shell.
|
||||
- [karousel]: scrollable tiling on top of KDE.
|
||||
- [papersway]: scrollable tiling on top of sway/i3.
|
||||
- [hyprscroller] and [hyprslidr]: scrollable tiling on top of Hyprland.
|
||||
- [PaperWM.spoon]: scrollable tiling on top of macOS.
|
||||
|
||||
## Contact
|
||||
|
||||
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
|
||||
|
||||
[PaperWM]: https://github.com/paperwm/PaperWM
|
||||
[mako]: https://github.com/emersion/mako
|
||||
[OBS]: https://flathub.org/apps/com.obsproject.Studio
|
||||
[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/
|
||||
[hyprscroller]: https://github.com/dawsers/hyprscroller
|
||||
[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": 1733064805,
|
||||
"narHash": "sha256-7NbtSLfZO0q7MXPl5hzA0sbVJt6pWxxtGWbaVUDDmjs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "fa9a51752f1b5de583ad5213eb621be071806663",
|
||||
"rev": "31d66ae40417bb13765b0ad75dd200400e98de84",
|
||||
"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": 1733106880,
|
||||
"narHash": "sha256-aJmAIjZfWfPSWSExwrYBLRgXVvgF5LP1vaeUGOOIQ98=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "e66c0d43abf5bdefb664c3583ca8994983c332ae",
|
||||
"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,106 +1,254 @@
|
||||
# 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,
|
||||
mesa,
|
||||
pango,
|
||||
pipewire,
|
||||
pkg-config,
|
||||
rustPlatform,
|
||||
systemd,
|
||||
wayland,
|
||||
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
|
||||
];
|
||||
|
||||
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
|
||||
mesa # 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
|
||||
];
|
||||
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 =
|
||||
''
|
||||
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,16 @@ 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.4", path = "../niri-ipc" }
|
||||
regex = "1.10.4"
|
||||
niri-ipc = { version = "25.1.0", 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
|
||||
miette = { version = "5.10.0", features = ["fancy"] }
|
||||
pretty_assertions = "1.4.1"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
use crate::{BlockOutFrom, RegexEq};
|
||||
|
||||
#[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>,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
pub struct Match {
|
||||
#[knuffel(property, str)]
|
||||
pub namespace: Option<RegexEq>,
|
||||
#[knuffel(property)]
|
||||
pub at_startup: Option<bool>,
|
||||
}
|
||||
+2352
-354
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")
|
||||
);
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -1,15 +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.21", 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.1.0"
|
||||
```
|
||||
+896
-91
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
//! Helper for blocking communication over the niri socket.
|
||||
|
||||
use std::env;
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::net::Shutdown;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{Event, Reply, Request};
|
||||
|
||||
/// Name of the environment variable containing the niri IPC socket path.
|
||||
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
|
||||
|
||||
/// Helper for blocking communication over the 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,
|
||||
}
|
||||
|
||||
impl Socket {
|
||||
/// Connects to the default niri IPC socket.
|
||||
///
|
||||
/// This is equivalent to calling [`Self::connect_to`] with the path taken from the
|
||||
/// [`SOCKET_PATH_ENV`] environment variable.
|
||||
pub fn connect() -> io::Result<Self> {
|
||||
let socket_path = env::var_os(SOCKET_PATH_ENV).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("{SOCKET_PATH_ENV} is not set, are you running this within niri?"),
|
||||
)
|
||||
})?;
|
||||
Self::connect_to(socket_path)
|
||||
}
|
||||
|
||||
/// 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())?;
|
||||
Ok(Self { stream })
|
||||
}
|
||||
|
||||
/// Sends a request to niri and returns the response.
|
||||
///
|
||||
/// Return values:
|
||||
///
|
||||
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
|
||||
/// * `Ok(Err(message))`: error message from niri
|
||||
/// * `Err(error)`: error communicating with niri
|
||||
///
|
||||
/// This method also returns a blocking function that you can call to keep reading [`Event`]s
|
||||
/// after requesting an [`EventStream`][Request::EventStream]. This function is not useful
|
||||
/// otherwise.
|
||||
pub fn send(self, request: Request) -> io::Result<(Reply, impl FnMut() -> io::Result<Event>)> {
|
||||
let Self { mut stream } = self;
|
||||
|
||||
let mut buf = serde_json::to_string(&request).unwrap();
|
||||
stream.write_all(buf.as_bytes())?;
|
||||
stream.shutdown(Shutdown::Write)?;
|
||||
|
||||
let mut reader = BufReader::new(stream);
|
||||
|
||||
buf.clear();
|
||||
reader.read_line(&mut buf)?;
|
||||
|
||||
let reply = serde_json::from_str(&buf)?;
|
||||
|
||||
let events = move || {
|
||||
buf.clear();
|
||||
reader.read_line(&mut buf)?;
|
||||
let event = serde_json::from_str(&buf)?;
|
||||
Ok(event)
|
||||
};
|
||||
|
||||
Ok((reply, events))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
//! 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,
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)?;
|
||||
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::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 => 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
|
||||
}
|
||||
}
|
||||
@@ -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.1", package = "libadwaita", features = ["v1_4"] }
|
||||
anyhow.workspace = true
|
||||
gtk = { version = "0.8.1", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "0.1.4", path = ".." }
|
||||
niri-config = { version = "0.1.4", path = "../niri-config" }
|
||||
gtk = { version = "0.9.5", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "25.1.0", path = ".." }
|
||||
niri-config = { version = "25.1.0", path = "../niri-config" }
|
||||
smithay.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
@@ -1,14 +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::gradient::GradientRenderElement;
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{Color, CornerRadius, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Scale, Size};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
use super::{Args, TestCase};
|
||||
|
||||
pub struct GradientAngle {
|
||||
angle: f32,
|
||||
@@ -16,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,
|
||||
@@ -30,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. {
|
||||
@@ -53,22 +45,26 @@ impl TestCase for GradientAngle {
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> 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();
|
||||
|
||||
GradientRenderElement::new(
|
||||
renderer,
|
||||
Scale::from(1.),
|
||||
area,
|
||||
area,
|
||||
[1., 0., 0., 1.],
|
||||
[0., 1., 0., 1.],
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
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_size(area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
|
||||
@@ -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::gradient::GradientRenderElement;
|
||||
use niri_config::Color;
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
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,16 +17,15 @@ pub struct GradientArea {
|
||||
}
|
||||
|
||||
impl GradientArea {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
let mut border = FocusRing::new(niri_config::FocusRing {
|
||||
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(),
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
});
|
||||
border.set_active(true);
|
||||
|
||||
Self {
|
||||
progress: 0.,
|
||||
@@ -44,20 +41,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. {
|
||||
@@ -75,38 +65,46 @@ 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 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(g_size, true);
|
||||
self.border.update_render_elements(
|
||||
g_size,
|
||||
true,
|
||||
true,
|
||||
Rectangle::default(),
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
);
|
||||
rv.extend(
|
||||
self.border
|
||||
.render(
|
||||
renderer,
|
||||
Point::from(g_loc),
|
||||
Scale::from(1.),
|
||||
size.to_logical(1),
|
||||
)
|
||||
.render(renderer, g_loc)
|
||||
.map(|elem| Box::new(elem) as _),
|
||||
);
|
||||
|
||||
rv.extend(
|
||||
GradientRenderElement::new(
|
||||
renderer,
|
||||
Scale::from(1.),
|
||||
area,
|
||||
[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_size(rect_size).to_f64(),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
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.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
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::scrolling::ColumnWidth;
|
||||
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};
|
||||
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 +20,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 +44,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,28 +58,31 @@ 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),
|
||||
active_gradient: None,
|
||||
inactive_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)));
|
||||
@@ -85,8 +97,8 @@ impl Layout {
|
||||
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| {
|
||||
@@ -99,8 +111,8 @@ 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| {
|
||||
@@ -113,8 +125,8 @@ 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)));
|
||||
@@ -129,8 +141,8 @@ impl Layout {
|
||||
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)));
|
||||
@@ -145,25 +157,54 @@ impl Layout {
|
||||
rv
|
||||
}
|
||||
|
||||
fn add_window(&mut self, window: TestWindow, width: Option<ColumnWidth>) {
|
||||
self.layout.add_window(window.clone(), width, false);
|
||||
if window.communicate() {
|
||||
self.layout.update_window(window.id());
|
||||
}
|
||||
fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) {
|
||||
let ws = self.layout.active_workspace().unwrap();
|
||||
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,
|
||||
None,
|
||||
);
|
||||
window.communicate();
|
||||
|
||||
self.layout.add_window(
|
||||
window.clone(),
|
||||
AddWindowTarget::Auto,
|
||||
width,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
ActivateWindow::default(),
|
||||
);
|
||||
self.windows.push(window);
|
||||
}
|
||||
|
||||
fn add_window_right_of(
|
||||
&mut self,
|
||||
right_of: &TestWindow,
|
||||
window: TestWindow,
|
||||
mut window: TestWindow,
|
||||
width: Option<ColumnWidth>,
|
||||
) {
|
||||
self.layout
|
||||
.add_window_right_of(right_of.id(), window.clone(), width, false);
|
||||
if window.communicate() {
|
||||
self.layout.update_window(window.id());
|
||||
}
|
||||
let ws = self.layout.active_workspace().unwrap();
|
||||
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,
|
||||
None,
|
||||
);
|
||||
window.communicate();
|
||||
|
||||
self.layout.add_window(
|
||||
window.clone(),
|
||||
AddWindowTarget::NextTo(right_of.id()),
|
||||
width,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
ActivateWindow::default(),
|
||||
);
|
||||
self.windows.push(window);
|
||||
}
|
||||
|
||||
@@ -184,35 +225,34 @@ impl TestCase for Layout {
|
||||
self.layout.update_output_size(&self.output);
|
||||
for win in &self.windows {
|
||||
if win.communicate() {
|
||||
self.layout.update_window(win.id());
|
||||
self.layout.update_window(win.id(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -220,11 +260,11 @@ impl TestCase for Layout {
|
||||
renderer: &mut GlesRenderer,
|
||||
_size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
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)
|
||||
.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, Scale, Size};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Scale, 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);
|
||||
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);
|
||||
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);
|
||||
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,20 +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) {
|
||||
self.tile.request_tile_size(Size::from((width, height)));
|
||||
let size = Size::from((width, height)).to_f64();
|
||||
self.tile
|
||||
.update_config(size, 1., self.tile.options().clone());
|
||||
self.tile.request_tile_size(size, false, None);
|
||||
self.window.communicate();
|
||||
}
|
||||
|
||||
@@ -92,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, true);
|
||||
fn advance_animations(&mut self, _current_time: Duration) {
|
||||
self.tile.advance_animations();
|
||||
}
|
||||
|
||||
fn render(
|
||||
@@ -101,15 +108,19 @@ 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(
|
||||
true,
|
||||
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
|
||||
);
|
||||
self.tile
|
||||
.render(
|
||||
renderer,
|
||||
location,
|
||||
Scale::from(1.),
|
||||
size.to_logical(1),
|
||||
true,
|
||||
RenderTarget::Output,
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
let window = TestWindow::freeform(0);
|
||||
window.request_size(size);
|
||||
pub fn freeform(args: Args) -> Self {
|
||||
let mut window = TestWindow::freeform(0);
|
||||
window.request_size(args.size, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
|
||||
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
window.request_size(size);
|
||||
pub fn fixed_size(args: Args) -> Self {
|
||||
let mut window = TestWindow::fixed_size(0);
|
||||
window.request_size(args.size, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
|
||||
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
|
||||
let window = TestWindow::fixed_size(0);
|
||||
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);
|
||||
window.request_size(args.size, 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)));
|
||||
self.window
|
||||
.request_size(Size::from((width, height)), 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,27 +1,29 @@
|
||||
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::render_helpers::shaders;
|
||||
use niri::utils::get_monotonic_time;
|
||||
use niri::animation::Clock;
|
||||
use niri::render_helpers::{resources, shaders};
|
||||
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;
|
||||
use smithay::backend::renderer::{Color32F, Frame, Renderer, Unbind};
|
||||
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>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SmithayView {
|
||||
@@ -30,6 +32,7 @@ mod imp {
|
||||
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
|
||||
pub make_test_case: OnceCell<DynMakeTestCase>,
|
||||
test_case: RefCell<Option<Box<dyn TestCase>>>,
|
||||
pub clock: RefCell<Clock>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
@@ -125,16 +128,24 @@ mod imp {
|
||||
|
||||
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| {
|
||||
@@ -147,7 +158,7 @@ mod imp {
|
||||
.context("error creating frame")?;
|
||||
|
||||
frame
|
||||
.clear([0.3, 0.3, 0.3, 1.], &[rect])
|
||||
.clear(Color32F::from([0.3, 0.3, 0.3, 1.]), &[rect])
|
||||
.context("error clearing")?;
|
||||
|
||||
for element in elements.iter().rev() {
|
||||
@@ -157,7 +168,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")?;
|
||||
}
|
||||
}
|
||||
@@ -186,14 +197,10 @@ 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")?;
|
||||
|
||||
resources::init(&mut renderer);
|
||||
shaders::init(&mut renderer);
|
||||
|
||||
Ok(renderer)
|
||||
@@ -237,14 +244,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,19 @@ use std::cell::RefCell;
|
||||
use std::cmp::{max, min};
|
||||
use std::rc::Rc;
|
||||
|
||||
use niri::layout::{LayoutElement, LayoutElementRenderElement};
|
||||
use niri::layout::{
|
||||
ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement,
|
||||
LayoutElementRenderSnapshot,
|
||||
};
|
||||
use niri::render_helpers::renderer::NiriRenderer;
|
||||
use niri::render_helpers::RenderTarget;
|
||||
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::output::{self, Output};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Scale, Size, Transform};
|
||||
use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestWindowInner {
|
||||
@@ -35,7 +39,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,
|
||||
@@ -47,7 +51,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]),
|
||||
})),
|
||||
}
|
||||
}
|
||||
@@ -83,7 +87,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);
|
||||
|
||||
@@ -110,14 +114,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
|
||||
}
|
||||
@@ -145,40 +149,46 @@ 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,
|
||||
) -> Vec<LayoutElementRenderElement<R>> {
|
||||
) -> SplitElements<LayoutElementRenderElement<R>> {
|
||||
let inner = self.inner.borrow();
|
||||
|
||||
vec![
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.buffer,
|
||||
location.to_physical_precise_round(scale),
|
||||
scale,
|
||||
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,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
]
|
||||
SplitElements {
|
||||
normal: vec![
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.buffer,
|
||||
location,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.csd_shadow_buffer,
|
||||
location
|
||||
- Point::from((inner.csd_shadow_width, inner.csd_shadow_width)).to_f64(),
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
],
|
||||
popups: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn request_size(&self, size: Size<i32, Logical>) {
|
||||
fn request_size(
|
||||
&mut self,
|
||||
size: Size<i32, Logical>,
|
||||
_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>) {
|
||||
fn request_fullscreen(&mut self, _size: Size<i32, Logical>) {
|
||||
self.inner.borrow_mut().pending_fullscreen = true;
|
||||
}
|
||||
|
||||
@@ -194,7 +204,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
|
||||
@@ -208,9 +218,17 @@ impl LayoutElement for TestWindow {
|
||||
|
||||
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 send_pending_configure(&self) {}
|
||||
fn configure_intent(&self) -> ConfigureIntent {
|
||||
ConfigureIntent::CanSend
|
||||
}
|
||||
|
||||
fn send_pending_configure(&mut self) {}
|
||||
|
||||
fn is_fullscreen(&self) -> bool {
|
||||
false
|
||||
@@ -220,10 +238,36 @@ 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 {
|
||||
static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty();
|
||||
&EMPTY
|
||||
}
|
||||
|
||||
fn animation_snapshot(&self) -> Option<&LayoutElementRenderSnapshot> {
|
||||
None
|
||||
}
|
||||
|
||||
fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_interactive_resize(&mut self, _data: Option<InteractiveResizeData>) {}
|
||||
|
||||
fn cancel_interactive_resize(&mut self) {}
|
||||
|
||||
fn on_commit(&mut self, _serial: Serial) {}
|
||||
|
||||
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
+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: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND (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) 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 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
|
||||
# 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
|
||||
export XDG_RUNTIME_DIR="$(mktemp -d)"
|
||||
%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 }}}
|
||||
|
||||
@@ -21,25 +21,41 @@ input {
|
||||
// Next sections include libinput settings.
|
||||
// Omitting settings disables them, or leaves them at their default values.
|
||||
touchpad {
|
||||
// off
|
||||
tap
|
||||
// dwt
|
||||
// dwtp
|
||||
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 +76,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 +123,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 +166,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
|
||||
|
||||
@@ -187,11 +207,14 @@ 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.
|
||||
// See the binds section below for more spawn examples.
|
||||
// spawn-at-startup "alacritty" "-e" "fish"
|
||||
|
||||
// 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 +250,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 +271,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
|
||||
@@ -259,12 +298,16 @@ binds {
|
||||
Mod+D { spawn "fuzzel"; }
|
||||
Super+Alt+L { 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.
|
||||
XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
|
||||
XF86AudioLowerVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
|
||||
// The allow-when-locked=true property makes them work even when the session is locked.
|
||||
XF86AudioRaiseVolume allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
|
||||
XF86AudioLowerVolume allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
|
||||
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"; }
|
||||
|
||||
Mod+Q { close-window; }
|
||||
|
||||
@@ -407,14 +450,20 @@ 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 { switch-preset-window-height; }
|
||||
Mod+Ctrl+R { reset-window-height; }
|
||||
Mod+F { maximize-column; }
|
||||
Mod+Shift+F { fullscreen-window; }
|
||||
Mod+C { center-column; }
|
||||
@@ -434,6 +483,10 @@ 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; }
|
||||
|
||||
// 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.
|
||||
@@ -448,6 +501,7 @@ binds {
|
||||
|
||||
// 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; 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; then
|
||||
# Check that the user dinit daemon is running
|
||||
if ! pgrep -u $(id -u) dinit &> /dev/null; 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; 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));
|
||||
}
|
||||
}
|
||||
+168
-116
@@ -1,23 +1,27 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use keyframe::functions::EaseOutCubic;
|
||||
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,
|
||||
initial_velocity: f64,
|
||||
is_off: bool,
|
||||
duration: Duration,
|
||||
/// Time until the animation first reaches `to`.
|
||||
///
|
||||
/// Best effort; not always exactly precise.
|
||||
clamped_duration: Duration,
|
||||
start_time: Duration,
|
||||
current_time: Duration,
|
||||
clock: Clock,
|
||||
kind: Kind,
|
||||
}
|
||||
|
||||
@@ -35,110 +39,165 @@ enum Kind {
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Curve {
|
||||
Linear,
|
||||
EaseOutQuad,
|
||||
EaseOutCubic,
|
||||
EaseOutExpo,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub fn new(
|
||||
clock: Clock,
|
||||
from: f64,
|
||||
to: f64,
|
||||
initial_velocity: f64,
|
||||
config: niri_config::Animation,
|
||||
default: 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(clock, from, to, initial_velocity, 0, Curve::EaseOutCubic);
|
||||
if config.off {
|
||||
return Self::ease(from, to, 0, Curve::EaseOutCubic);
|
||||
rv.is_off = true;
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Resolve defaults.
|
||||
let (kind, easing_defaults) = match (config.kind, default.kind) {
|
||||
// Configured spring.
|
||||
(configured @ niri_config::AnimationKind::Spring(_), _) => (configured, None),
|
||||
// Configured nothing, defaults spring.
|
||||
(
|
||||
niri_config::AnimationKind::Easing(easing),
|
||||
defaults @ niri_config::AnimationKind::Spring(_),
|
||||
) if easing == niri_config::EasingParams::unfilled() => (defaults, None),
|
||||
// Configured easing or nothing, defaults easing.
|
||||
(
|
||||
configured @ niri_config::AnimationKind::Easing(_),
|
||||
niri_config::AnimationKind::Easing(defaults),
|
||||
) => (configured, Some(defaults)),
|
||||
// Configured easing, defaults spring.
|
||||
(
|
||||
configured @ niri_config::AnimationKind::Easing(_),
|
||||
niri_config::AnimationKind::Spring(_),
|
||||
) => (configured, None),
|
||||
};
|
||||
rv.replace_config(config);
|
||||
rv
|
||||
}
|
||||
|
||||
match kind {
|
||||
pub fn replace_config(&mut self, config: niri_config::Animation) {
|
||||
self.is_off = config.off;
|
||||
if config.off {
|
||||
self.duration = Duration::ZERO;
|
||||
self.clamped_duration = Duration::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
let start_time = self.start_time;
|
||||
|
||||
match config.kind {
|
||||
niri_config::AnimationKind::Spring(p) => {
|
||||
let params = SpringParams::new(p.damping_ratio, f64::from(p.stiffness), p.epsilon);
|
||||
|
||||
let spring = Spring {
|
||||
from,
|
||||
to,
|
||||
initial_velocity,
|
||||
from: self.from,
|
||||
to: self.to,
|
||||
initial_velocity: self.initial_velocity,
|
||||
params,
|
||||
};
|
||||
Self::spring(spring)
|
||||
*self = Self::spring(self.clock.clone(), spring);
|
||||
}
|
||||
niri_config::AnimationKind::Easing(p) => {
|
||||
let defaults = easing_defaults.unwrap_or(niri_config::EasingParams::default());
|
||||
let duration_ms = p.duration_ms.or(defaults.duration_ms).unwrap();
|
||||
let curve = Curve::from(p.curve.or(defaults.curve).unwrap());
|
||||
Self::ease(from, to, u64::from(duration_ms), curve)
|
||||
*self = Self::ease(
|
||||
self.clock.clone(),
|
||||
self.from,
|
||||
self.to,
|
||||
self.initial_velocity,
|
||||
u64::from(p.duration_ms),
|
||||
Curve::from(p.curve),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.start_time = start_time;
|
||||
}
|
||||
|
||||
/// Restarts the animation using the previous config.
|
||||
pub fn restarted(&self, from: f64, to: f64, initial_velocity: f64) -> Self {
|
||||
if self.is_off {
|
||||
return self.clone();
|
||||
}
|
||||
|
||||
// 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,
|
||||
self.duration.as_millis() as u64,
|
||||
curve,
|
||||
),
|
||||
Kind::Spring(spring) => {
|
||||
let spring = Spring {
|
||||
from: self.from,
|
||||
to: self.to,
|
||||
initial_velocity: self.initial_velocity,
|
||||
params: spring.params,
|
||||
};
|
||||
Self::spring(self.clock.clone(), spring)
|
||||
}
|
||||
Kind::Deceleration {
|
||||
initial_velocity,
|
||||
deceleration_rate,
|
||||
} => {
|
||||
let threshold = 0.001; // FIXME
|
||||
Self::decelerate(
|
||||
self.clock.clone(),
|
||||
from,
|
||||
initial_velocity,
|
||||
deceleration_rate,
|
||||
threshold,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ease(from: f64, to: 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 };
|
||||
|
||||
Self {
|
||||
from,
|
||||
to,
|
||||
initial_velocity,
|
||||
is_off: false,
|
||||
duration,
|
||||
start_time: now,
|
||||
current_time: now,
|
||||
// Our current curves never overshoot.
|
||||
clamped_duration: duration,
|
||||
start_time: clock.now(),
|
||||
clock,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spring(spring: Spring) -> 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 spring(clock: Clock, spring: Spring) -> Self {
|
||||
let _span = tracy_client::span!("Animation::spring");
|
||||
|
||||
let duration = spring.duration();
|
||||
let clamped_duration = spring.clamped_duration().unwrap_or(duration);
|
||||
let kind = Kind::Spring(spring);
|
||||
|
||||
Self {
|
||||
from: spring.from,
|
||||
to: spring.to,
|
||||
initial_velocity: spring.initial_velocity,
|
||||
is_off: false,
|
||||
duration,
|
||||
start_time: now,
|
||||
current_time: now,
|
||||
clamped_duration,
|
||||
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 {
|
||||
@@ -157,74 +216,30 @@ impl Animation {
|
||||
Self {
|
||||
from,
|
||||
to,
|
||||
initial_velocity,
|
||||
is_off: false,
|
||||
duration,
|
||||
start_time: now,
|
||||
current_time: now,
|
||||
clamped_duration: duration,
|
||||
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;
|
||||
pub fn is_done(&self) -> bool {
|
||||
if self.clock.should_complete_instantly() {
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
self.clock.now() >= self.start_time + self.duration
|
||||
}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.current_time >= self.start_time + self.duration
|
||||
pub fn is_clamped_done(&self) -> bool {
|
||||
if self.clock.should_complete_instantly() {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.clock.now() >= self.start_time + self.clamped_duration
|
||||
}
|
||||
|
||||
pub fn value(&self) -> f64 {
|
||||
@@ -232,7 +247,7 @@ impl Animation {
|
||||
return self.to;
|
||||
}
|
||||
|
||||
let passed = self.current_time - self.start_time;
|
||||
let passed = self.clock.now().saturating_sub(self.start_time);
|
||||
|
||||
match self.kind {
|
||||
Kind::Easing { curve } => {
|
||||
@@ -241,7 +256,19 @@ impl Animation {
|
||||
let x = (passed / total).clamp(0., 1.);
|
||||
curve.y(x) * (self.to - self.from) + self.from
|
||||
}
|
||||
Kind::Spring(spring) => spring.value_at(passed),
|
||||
Kind::Spring(spring) => {
|
||||
let value = spring.value_at(passed);
|
||||
|
||||
// Protect against numerical instability.
|
||||
let range = (self.to - self.from) * 10.;
|
||||
let a = self.from - range;
|
||||
let b = self.to + range;
|
||||
if self.from <= self.to {
|
||||
value.clamp(a, b)
|
||||
} else {
|
||||
value.clamp(b, a)
|
||||
}
|
||||
}
|
||||
Kind::Deceleration {
|
||||
initial_velocity,
|
||||
deceleration_rate,
|
||||
@@ -253,6 +280,17 @@ impl Animation {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a value that stops at the target value after first reaching it.
|
||||
///
|
||||
/// Best effort; not always exactly precise.
|
||||
pub fn clamped_value(&self) -> f64 {
|
||||
if self.is_clamped_done() {
|
||||
return self.to;
|
||||
}
|
||||
|
||||
self.value()
|
||||
}
|
||||
|
||||
pub fn to(&self) -> f64 {
|
||||
self.to
|
||||
}
|
||||
@@ -261,11 +299,23 @@ impl Animation {
|
||||
pub fn from(&self) -> f64 {
|
||||
self.from
|
||||
}
|
||||
|
||||
pub fn offset(&mut self, offset: f64) {
|
||||
self.from += offset;
|
||||
self.to += offset;
|
||||
|
||||
if let Kind::Spring(spring) = &mut self.kind {
|
||||
spring.from += offset;
|
||||
spring.to += offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Curve {
|
||||
pub fn y(self, x: f64) -> f64 {
|
||||
match self {
|
||||
Curve::Linear => x,
|
||||
Curve::EaseOutQuad => EaseOutQuad.y(x),
|
||||
Curve::EaseOutCubic => EaseOutCubic.y(x),
|
||||
Curve::EaseOutExpo => 1. - 2f64.powf(-10. * x),
|
||||
}
|
||||
@@ -275,6 +325,8 @@ impl Curve {
|
||||
impl From<niri_config::AnimationCurve> for Curve {
|
||||
fn from(value: niri_config::AnimationCurve) -> Self {
|
||||
match value {
|
||||
niri_config::AnimationCurve::Linear => Curve::Linear,
|
||||
niri_config::AnimationCurve::EaseOutQuad => Curve::EaseOutQuad,
|
||||
niri_config::AnimationCurve::EaseOutCubic => Curve::EaseOutCubic,
|
||||
niri_config::AnimationCurve::EaseOutExpo => Curve::EaseOutExpo,
|
||||
}
|
||||
|
||||
@@ -96,6 +96,37 @@ impl Spring {
|
||||
Duration::from_secs_f64(x1)
|
||||
}
|
||||
|
||||
/// Computes and returns the duration until the spring reaches its target position.
|
||||
pub fn clamped_duration(&self) -> Option<Duration> {
|
||||
let beta = self.params.damping / (2. * self.params.mass);
|
||||
|
||||
if beta.abs() <= f64::EPSILON || beta < 0. {
|
||||
return Some(Duration::MAX);
|
||||
}
|
||||
|
||||
if (self.to - self.from).abs() <= f64::EPSILON {
|
||||
return Some(Duration::ZERO);
|
||||
}
|
||||
|
||||
// The first frame is not that important and we avoid finding the trivial 0 for in-place
|
||||
// animations.
|
||||
let mut i = 1u16;
|
||||
let mut y = self.oscillate(f64::from(i) / 1000.);
|
||||
|
||||
while (self.to - self.from > f64::EPSILON && self.to - y > self.params.epsilon)
|
||||
|| (self.from - self.to > f64::EPSILON && y - self.to > self.params.epsilon)
|
||||
{
|
||||
if i > 3000 {
|
||||
return None;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
y = self.oscillate(f64::from(i) / 1000.);
|
||||
}
|
||||
|
||||
Some(Duration::from_millis(u64::from(i)))
|
||||
}
|
||||
|
||||
/// Returns the spring position at a given time in seconds.
|
||||
fn oscillate(&self, t: f64) -> f64 {
|
||||
let b = self.params.damping;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
+59
-1
@@ -9,6 +9,7 @@ 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,13 @@ pub use tty::Tty;
|
||||
pub mod winit;
|
||||
pub use winit::Winit;
|
||||
|
||||
pub mod headless;
|
||||
pub use headless::Headless;
|
||||
|
||||
pub enum Backend {
|
||||
Tty(Tty),
|
||||
Winit(Winit),
|
||||
Headless(Headless),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
@@ -31,13 +36,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 +66,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 +77,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,6 +90,7 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +98,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(_) => CompositorMod::Super,
|
||||
Backend::Winit(_) => CompositorMod::Alt,
|
||||
Backend::Headless(_) => CompositorMod::Super,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +106,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.change_vt(vt),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +114,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.suspend(),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +122,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.toggle_debug_tint(),
|
||||
Backend::Winit(winit) => winit.toggle_debug_tint(),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +130,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 +138,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.early_import(surface),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +146,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 +158,7 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.primary_gbm_device(),
|
||||
Backend::Winit(_) => None,
|
||||
Backend::Headless(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +166,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,6 +182,15 @@ impl Backend {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.on_output_config_changed(niri),
|
||||
Backend::Winit(_) => (),
|
||||
Backend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tty_checked(&mut self) -> Option<&mut Tty> {
|
||||
if let Self::Tty(v) = self {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,4 +209,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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+707
-306
File diff suppressed because it is too large
Load Diff
+48
-16
@@ -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,11 +14,13 @@ 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::{shaders, RenderTarget};
|
||||
use crate::render_helpers::debug::draw_damage;
|
||||
use crate::render_helpers::{resources, shaders, RenderTarget};
|
||||
use crate::utils::{get_monotonic_time, logical_output};
|
||||
|
||||
pub struct Winit {
|
||||
@@ -35,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(),
|
||||
@@ -58,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,
|
||||
@@ -73,6 +82,8 @@ impl Winit {
|
||||
is_preferred: true,
|
||||
}],
|
||||
current_mode: Some(0),
|
||||
vrr_supported: false,
|
||||
vrr_enabled: false,
|
||||
logical: Some(logical_output(&output)),
|
||||
},
|
||||
)])));
|
||||
@@ -95,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;
|
||||
@@ -130,9 +141,24 @@ impl Winit {
|
||||
warn!("error binding renderer wl_display: {err}");
|
||||
}
|
||||
|
||||
resources::init(renderer);
|
||||
shaders::init(renderer);
|
||||
|
||||
niri.add_output(self.output.clone(), None);
|
||||
let config = self.config.borrow();
|
||||
if let Some(src) = config.animations.window_resize.custom_shader.as_deref() {
|
||||
shaders::set_custom_resize_program(renderer, Some(src));
|
||||
}
|
||||
if let Some(src) = config.animations.window_close.custom_shader.as_deref() {
|
||||
shaders::set_custom_close_program(renderer, Some(src));
|
||||
}
|
||||
if let Some(src) = config.animations.window_open.custom_shader.as_deref() {
|
||||
shaders::set_custom_open_program(renderer, Some(src));
|
||||
}
|
||||
drop(config);
|
||||
|
||||
niri.layout.update_shaders();
|
||||
|
||||
niri.add_output(self.output.clone(), None, false);
|
||||
}
|
||||
|
||||
pub fn seat_name(&self) -> String {
|
||||
@@ -150,13 +176,19 @@ impl Winit {
|
||||
let _span = tracy_client::span!("Winit::render");
|
||||
|
||||
// Render the elements.
|
||||
let elements = niri.render::<GlesRenderer>(
|
||||
let mut elements = niri.render::<GlesRenderer>(
|
||||
self.backend.renderer(),
|
||||
output,
|
||||
true,
|
||||
RenderTarget::Output,
|
||||
);
|
||||
|
||||
// Visualize the damage, if enabled.
|
||||
if niri.debug_draw_damage {
|
||||
let output_state = niri.output_state.get_mut(output).unwrap();
|
||||
draw_damage(&mut output_state.debug_damage_tracker, &mut elements);
|
||||
}
|
||||
|
||||
// Hand them over to winit.
|
||||
self.backend.bind().unwrap();
|
||||
let age = self.backend.buffer_age().unwrap();
|
||||
@@ -176,17 +208,17 @@ impl Winit {
|
||||
.wait_for_frame_completion_before_queueing
|
||||
{
|
||||
let _span = tracy_client::span!("wait for completion");
|
||||
res.sync.wait();
|
||||
if let Err(err) = res.sync.wait() {
|
||||
warn!("error waiting for frame completion: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
self.backend.submit(Some(&damage)).unwrap();
|
||||
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(),
|
||||
);
|
||||
|
||||
+44
-7
@@ -2,7 +2,7 @@ use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use niri_ipc::Action;
|
||||
use niri_ipc::{Action, OutputAction};
|
||||
|
||||
use crate::utils::version;
|
||||
|
||||
@@ -13,6 +13,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.
|
||||
@@ -32,12 +35,6 @@ pub struct Cli {
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Sub {
|
||||
/// Validate the config file.
|
||||
Validate {
|
||||
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||
#[arg(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
},
|
||||
/// Communicate with the running niri instance.
|
||||
Msg {
|
||||
#[command(subcommand)]
|
||||
@@ -46,6 +43,15 @@ pub enum Sub {
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
},
|
||||
/// 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,
|
||||
}
|
||||
@@ -54,6 +60,16 @@ pub enum Sub {
|
||||
pub enum Msg {
|
||||
/// List connected outputs.
|
||||
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,
|
||||
/// Perform an action.
|
||||
@@ -61,4 +77,25 @@ pub enum Msg {
|
||||
#[command(subcommand)]
|
||||
action: Action,
|
||||
},
|
||||
/// Change output configuration temporarily.
|
||||
///
|
||||
/// The configuration is changed temporarily and not saved into the config file. If the output
|
||||
/// configuration subsequently changes in the config file, these temporary changes will be
|
||||
/// forgotten.
|
||||
Output {
|
||||
/// Output name.
|
||||
///
|
||||
/// Run `niri msg outputs` to see the output names.
|
||||
#[arg()]
|
||||
output: String,
|
||||
/// Configuration to apply.
|
||||
#[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,
|
||||
}
|
||||
|
||||
+1
-1
@@ -142,7 +142,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,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use zbus::dbus_interface;
|
||||
use zbus::fdo::{self, RequestNameFlags};
|
||||
use zbus::interface;
|
||||
|
||||
use super::Start;
|
||||
|
||||
@@ -18,7 +18,7 @@ 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,
|
||||
|
||||
+19
-8
@@ -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>,
|
||||
}
|
||||
@@ -66,24 +69,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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::Serialize;
|
||||
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;
|
||||
|
||||
pub struct DisplayConfig {
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
@@ -42,7 +44,7 @@ pub struct LogicalMonitor {
|
||||
properties: HashMap<String, OwnedValue>,
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.gnome.Mutter.DisplayConfig")]
|
||||
#[interface(name = "org.gnome.Mutter.DisplayConfig")]
|
||||
impl DisplayConfig {
|
||||
async fn get_current_state(
|
||||
&self,
|
||||
@@ -57,24 +59,20 @@ impl DisplayConfig {
|
||||
.ipc_outputs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.values()
|
||||
// Take only enabled outputs.
|
||||
.filter(|(_, output)| output.current_mode.is_some() && output.logical.is_some())
|
||||
.map(|(c, output)| {
|
||||
.filter(|output| output.current_mode.is_some() && output.logical.is_some())
|
||||
.map(|output| {
|
||||
// Loosely matches the check in Mutter.
|
||||
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
|
||||
|
||||
// FIXME: use proper serial when we have libdisplay-info.
|
||||
// A serial is required for correct session restore by xdp-gnome.
|
||||
let serial = c.clone();
|
||||
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("display-name"),
|
||||
OwnedValue::from(zvariant::Str::from(display_name)),
|
||||
);
|
||||
properties.insert(
|
||||
String::from("is-builtin"),
|
||||
OwnedValue::from(is_laptop_panel),
|
||||
@@ -110,8 +108,16 @@ impl DisplayConfig {
|
||||
.properties
|
||||
.insert(String::from("is-current"), OwnedValue::from(true));
|
||||
|
||||
let connector = c.clone();
|
||||
let model = output.model.clone();
|
||||
let make = output.make.clone();
|
||||
|
||||
// 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 monitor = Monitor {
|
||||
names: (c.clone(), String::new(), String::new(), serial),
|
||||
names: (connector, make, model, serial),
|
||||
modes,
|
||||
properties,
|
||||
};
|
||||
@@ -143,23 +149,16 @@ impl DisplayConfig {
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 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.0.names.0.cmp(&b.0.names.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<()>;
|
||||
#[zbus(signal)]
|
||||
pub async fn monitors_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
impl DisplayConfig {
|
||||
@@ -182,3 +181,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″");
|
||||
}
|
||||
}
|
||||
|
||||
+117
-32
@@ -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,40 @@ 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,
|
||||
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 +93,16 @@ struct StreamParameters {
|
||||
pub enum ScreenCastToNiri {
|
||||
StartCast {
|
||||
session_id: usize,
|
||||
output: String,
|
||||
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 +137,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(self.id, 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 +176,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 +192,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 +204,16 @@ 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)
|
||||
STREAM_ID.fetch_add(1, Ordering::SeqCst)
|
||||
);
|
||||
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(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 +230,68 @@ 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 path = format!(
|
||||
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
|
||||
STREAM_ID.fetch_add(1, Ordering::SeqCst)
|
||||
);
|
||||
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(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 +349,27 @@ impl Drop for Session {
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
pub fn new(
|
||||
output: niri_ipc::Output,
|
||||
fn new(
|
||||
target: StreamTarget,
|
||||
cursor_mode: CursorMode,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
) -> Self {
|
||||
Self {
|
||||
output,
|
||||
target,
|
||||
cursor_mode,
|
||||
was_started: Arc::new(AtomicBool::new(false)),
|
||||
to_niri,
|
||||
}
|
||||
}
|
||||
|
||||
fn start(&self, session_id: usize, ctxt: SignalContext<'static>) {
|
||||
fn start(&self, session_id: usize, ctxt: SignalEmitter<'static>) {
|
||||
if self.was_started.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
let msg = ScreenCastToNiri::StartCast {
|
||||
session_id,
|
||||
output: self.output.name.clone(),
|
||||
target: self.target.make_id(),
|
||||
cursor_mode: self.cursor_mode,
|
||||
signal_ctx: ctxt,
|
||||
};
|
||||
@@ -305,3 +379,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,9 +1,8 @@
|
||||
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;
|
||||
@@ -12,14 +11,14 @@ pub struct ServiceChannel {
|
||||
display: DisplayHandle,
|
||||
}
|
||||
|
||||
#[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(),
|
||||
));
|
||||
}
|
||||
@@ -30,9 +29,11 @@ impl ServiceChannel {
|
||||
// Would be nice to thread config here but for now it's fine.
|
||||
can_view_decoration_globals: false,
|
||||
restricted: false,
|
||||
// FIXME: maybe you can get the PID from D-Bus somehow?
|
||||
credentials_unknown: true,
|
||||
});
|
||||
self.display.insert_client(sock2, data).unwrap();
|
||||
Ok(unsafe { zbus::zvariant::OwnedFd::from_raw_fd(sock1.into_raw_fd()) })
|
||||
Ok(zvariant::OwnedFd::from(std::os::fd::OwnedFd::from(sock1)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +45,7 @@ impl ServiceChannel {
|
||||
|
||||
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()?;
|
||||
|
||||
+24
-2
@@ -7,10 +7,11 @@ use crate::utils::get_monotonic_time;
|
||||
pub struct FrameClock {
|
||||
last_presentation_time: Option<Duration>,
|
||||
refresh_interval_ns: Option<NonZeroU64>,
|
||||
vrr: bool,
|
||||
}
|
||||
|
||||
impl FrameClock {
|
||||
pub fn new(refresh_interval: Option<Duration>) -> Self {
|
||||
pub fn new(refresh_interval: Option<Duration>, vrr: bool) -> Self {
|
||||
let refresh_interval_ns = if let Some(interval) = &refresh_interval {
|
||||
assert_eq!(interval.as_secs(), 0);
|
||||
Some(NonZeroU64::new(interval.subsec_nanos().into()).unwrap())
|
||||
@@ -21,6 +22,7 @@ impl FrameClock {
|
||||
Self {
|
||||
last_presentation_time: None,
|
||||
refresh_interval_ns,
|
||||
vrr,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +31,19 @@ impl FrameClock {
|
||||
.map(|r| Duration::from_nanos(r.get()))
|
||||
}
|
||||
|
||||
pub fn set_vrr(&mut self, vrr: bool) {
|
||||
if self.vrr == vrr {
|
||||
return;
|
||||
}
|
||||
|
||||
self.vrr = vrr;
|
||||
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.
|
||||
@@ -71,6 +86,13 @@ impl FrameClock {
|
||||
let since_last_ns =
|
||||
since_last.as_secs() * 1_000_000_000 + u64::from(since_last.subsec_nanos());
|
||||
let to_next_ns = (since_last_ns / refresh_interval_ns + 1) * refresh_interval_ns;
|
||||
last_presentation_time + Duration::from_nanos(to_next_ns)
|
||||
|
||||
// If VRR is enabled and more than one frame passed since last presentation, assume that we
|
||||
// can present immediately.
|
||||
if self.vrr && to_next_ns > refresh_interval_ns {
|
||||
now
|
||||
} else {
|
||||
last_presentation_time + Duration::from_nanos(to_next_ns)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+300
-67
@@ -1,22 +1,29 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use niri_ipc::PositionChange;
|
||||
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
|
||||
use smithay::input::pointer::CursorImageStatus;
|
||||
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,
|
||||
};
|
||||
use smithay::wayland::dmabuf::get_dmabuf;
|
||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||
use smithay::wayland::shm::{ShmHandler, ShmState};
|
||||
use smithay::{delegate_compositor, delegate_shm};
|
||||
|
||||
use super::xdg_shell::add_mapped_toplevel_pre_commit_hook;
|
||||
use crate::handlers::XDG_ACTIVATION_TOKEN_TIMEOUT;
|
||||
use crate::layout::{ActivateWindow, AddWindowTarget};
|
||||
use crate::niri::{ClientState, State};
|
||||
use crate::utils::send_scale_transform;
|
||||
use crate::utils::transaction::Transaction;
|
||||
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
|
||||
|
||||
impl CompositorHandler for State {
|
||||
@@ -35,50 +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).ok(),
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
if let Some(dmabuf) = maybe_dmabuf {
|
||||
if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
|
||||
let client = surface.client().unwrap();
|
||||
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);
|
||||
@@ -92,6 +70,11 @@ impl CompositorHandler for State {
|
||||
root_surface = parent;
|
||||
}
|
||||
|
||||
// Update the cached root surface.
|
||||
self.niri
|
||||
.root_surface
|
||||
.insert(surface.clone(), root_surface.clone());
|
||||
|
||||
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()) {
|
||||
@@ -104,31 +87,76 @@ impl CompositorHandler for State {
|
||||
|
||||
if is_mapped {
|
||||
// The toplevel got mapped.
|
||||
let Unmapped { window, state } = entry.remove();
|
||||
let Unmapped {
|
||||
window,
|
||||
state,
|
||||
activation_token_data,
|
||||
} = entry.remove();
|
||||
|
||||
window.on_commit();
|
||||
|
||||
let (rules, width, is_full_width, output) =
|
||||
let toplevel = window.toplevel().expect("no X11 support");
|
||||
|
||||
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,
|
||||
} = state
|
||||
{
|
||||
// Check that the output is still connected.
|
||||
let output =
|
||||
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
|
||||
|
||||
(rules, width, is_full_width, output)
|
||||
// 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, height, is_full_width, output, workspace_id)
|
||||
} else {
|
||||
error!("window map must happen after initial configure");
|
||||
(ResolvedWindowRules::empty(), None, false, None)
|
||||
(ResolvedWindowRules::empty(), None, None, false, None, None)
|
||||
};
|
||||
|
||||
let parent = window
|
||||
.toplevel()
|
||||
.expect("no x11 support")
|
||||
// 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))
|
||||
// Only consider the parent if we configured the window for the same
|
||||
@@ -142,30 +170,40 @@ impl CompositorHandler for State {
|
||||
})
|
||||
.map(|(mapped, _)| mapped.window.clone());
|
||||
|
||||
let mapped = Mapped::new(window, rules);
|
||||
// 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)
|
||||
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);
|
||||
@@ -187,7 +225,8 @@ impl CompositorHandler for State {
|
||||
let window = mapped.window.clone();
|
||||
let output = output.clone();
|
||||
|
||||
window.on_commit();
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
let id = mapped.id();
|
||||
|
||||
// This is a commit of a previously-mapped toplevel.
|
||||
let is_mapped =
|
||||
@@ -197,9 +236,44 @@ impl CompositorHandler for State {
|
||||
false
|
||||
});
|
||||
|
||||
// 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, blocker);
|
||||
});
|
||||
}
|
||||
|
||||
window.on_commit();
|
||||
|
||||
if !is_mapped {
|
||||
// The toplevel got unmapped.
|
||||
self.niri.layout.remove_window(&window);
|
||||
//
|
||||
// Test client: wleird-unmap.
|
||||
let active_window = self.niri.layout.focus().map(|m| &m.window);
|
||||
let was_active = active_window == Some(&window);
|
||||
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
self.niri
|
||||
.stop_casts_for_target(crate::pw_utils::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();
|
||||
}
|
||||
|
||||
// Newly-unmapped toplevels must perform the initial commit-configure sequence
|
||||
// afresh.
|
||||
@@ -210,11 +284,44 @@ impl CompositorHandler for State {
|
||||
return;
|
||||
}
|
||||
|
||||
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, buffer_delta)
|
||||
});
|
||||
if serial.is_none() {
|
||||
error!("commit on a mapped surface without a configured serial");
|
||||
}
|
||||
|
||||
// The toplevel remains mapped.
|
||||
self.niri.layout.update_window(&window);
|
||||
self.niri.layout.update_window(&window, serial);
|
||||
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Popup placement depends on window size which might have changed.
|
||||
self.update_reactive_popups(&window, &output);
|
||||
self.update_reactive_popups(&window);
|
||||
|
||||
self.niri.queue_redraw(&output);
|
||||
return;
|
||||
@@ -229,7 +336,7 @@ impl CompositorHandler for State {
|
||||
let window = mapped.window.clone();
|
||||
let output = output.clone();
|
||||
window.on_commit();
|
||||
self.niri.layout.update_window(&window);
|
||||
self.niri.layout.update_window(&window, None);
|
||||
self.niri.queue_redraw(&output);
|
||||
return;
|
||||
}
|
||||
@@ -240,36 +347,108 @@ 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() == surface {
|
||||
if lock_surface.wl_surface() == &root_surface {
|
||||
self.niri.queue_redraw(&output.clone());
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyed(&mut self, surface: &WlSurface) {
|
||||
// Clients may destroy their subsurfaces before the main surface. Ensure we have a snapshot
|
||||
// when that happens, so that the closing animation includes all these subsurfaces.
|
||||
//
|
||||
// Test client: alacritty with CSD <= 0.13 (it was fixed in winit afterwards:
|
||||
// https://github.com/rust-windowing/winit/pull/3625).
|
||||
//
|
||||
// This is still not perfect, as this function is called already after the (first)
|
||||
// subsurface is destroyed; in the case of alacritty, this is the top CSD shadow. But, it
|
||||
// gets most of the job done.
|
||||
if let Some(root) = self.niri.root_surface.get(surface) {
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
|
||||
let window = mapped.window.clone();
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
self.niri.layout.store_unmap_snapshot(renderer, &window);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self.niri
|
||||
.root_surface
|
||||
.retain(|k, v| k != surface && v != surface);
|
||||
|
||||
self.niri.dmabuf_pre_commit_hook.remove(surface);
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferHandler for State {
|
||||
@@ -284,3 +463,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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+120
-38
@@ -1,16 +1,19 @@
|
||||
use smithay::backend::renderer::utils::with_renderer_surface_state;
|
||||
use smithay::delegate_layer_shell;
|
||||
use smithay::desktop::{layer_map_for_output, LayerSurface, WindowSurfaceType};
|
||||
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
|
||||
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::send_scale_transform;
|
||||
|
||||
impl WlrLayerShellHandler for State {
|
||||
fn shell_state(&mut self) -> &mut WlrLayerShellState {
|
||||
@@ -24,17 +27,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 +61,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
|
||||
@@ -55,58 +72,123 @@ impl WlrLayerShellHandler for State {
|
||||
}
|
||||
|
||||
fn new_popup(&mut self, _parent: WlrLayerSurface, popup: PopupSurface) {
|
||||
self.unconstrain_popup(&popup);
|
||||
self.unconstrain_popup(&PopupKind::Xdg(popup));
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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
|
||||
});
|
||||
|
||||
layer.layer_surface().send_configure();
|
||||
if is_mapped {
|
||||
let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface);
|
||||
|
||||
// Resolve rules for newly mapped layer surfaces.
|
||||
if was_unmapped {
|
||||
let rules = &self.niri.config.borrow().layer_rules;
|
||||
let rules =
|
||||
ResolvedLayerRules::compute(rules, layer, self.niri.is_at_startup);
|
||||
let mapped = MappedLayer::new(layer.clone(), rules);
|
||||
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 {
|
||||
self.niri.mapped_layer_surfaces.remove(layer);
|
||||
self.niri.unmapped_layer_surfaces.insert(surface.clone());
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
+276
-43
@@ -7,29 +7,36 @@ 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::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::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,
|
||||
};
|
||||
@@ -45,23 +52,36 @@ 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_fractional_scale, 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_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::niri::{ClientState, DndIcon, 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::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,
|
||||
};
|
||||
|
||||
pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
impl SeatHandler for State {
|
||||
type KeyboardFocus = WlSurface;
|
||||
@@ -105,41 +125,115 @@ impl SeatHandler for State {
|
||||
}
|
||||
delegate_seat!(State);
|
||||
delegate_cursor_shape!(State);
|
||||
delegate_tablet_manager!(State);
|
||||
delegate_pointer_gestures!(State);
|
||||
delegate_relative_pointer!(State);
|
||||
delegate_text_input_manager!(State);
|
||||
|
||||
impl TabletSeatHandler for State {
|
||||
fn tablet_tool_image(&mut self, _tool: &TabletToolDescriptor, image: CursorImageStatus) {
|
||||
// FIXME: tablet tools should have their own cursors.
|
||||
self.niri.cursor_manager.set_cursor_image(image);
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
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_hidden {
|
||||
// FIXME: redraw only outputs overlapping the cursor.
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate_pointer_constraints!(State);
|
||||
|
||||
impl InputMethodHandler for State {
|
||||
fn new_popup(&mut self, surface: PopupSurface) {
|
||||
let popup = PopupKind::from(surface.clone());
|
||||
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 = surface.wl_surface();
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
self.unconstrain_popup(&popup);
|
||||
|
||||
if let Err(err) = self.niri.popups.track_popup(popup) {
|
||||
warn!("error tracking ime popup {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn popup_repositioned(&mut self, surface: PopupSurface) {
|
||||
let popup = PopupKind::InputMethod(surface);
|
||||
self.unconstrain_popup(&popup);
|
||||
}
|
||||
|
||||
fn dismiss_popup(&mut self, surface: PopupSurface) {
|
||||
if let Some(parent) = surface.get_parent().map(|parent| parent.surface.clone()) {
|
||||
let _ = PopupManager::dismiss_popup(&parent, &PopupKind::from(surface));
|
||||
}
|
||||
}
|
||||
|
||||
fn parent_geometry(&self, parent: &WlSurface) -> Rectangle<i32, Logical> {
|
||||
self.niri
|
||||
.layout
|
||||
@@ -167,6 +261,10 @@ impl SelectionHandler for State {
|
||||
|
||||
let buf = user_data.clone();
|
||||
thread::spawn(move || {
|
||||
// Clear O_NONBLOCK, otherwise File::write_all() will stop halfway.
|
||||
if let Err(err) = fcntl_setfl(&fd, OFlags::empty()) {
|
||||
warn!("error clearing flags on selection target fd: {err:?}");
|
||||
}
|
||||
if let Err(err) = File::from(fd).write_all(&buf) {
|
||||
warn!("error writing selection: {err:?}");
|
||||
}
|
||||
@@ -187,12 +285,61 @@ 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}");
|
||||
|
||||
// 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() {
|
||||
if let Some(root) = self.niri.root_surface.get(&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_hidden {
|
||||
// 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.activate_output(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.dnd_icon = None;
|
||||
// FIXME: more granular
|
||||
self.niri.queue_redraw_all();
|
||||
@@ -258,11 +405,15 @@ impl SessionLockHandler for State {
|
||||
|
||||
fn unlock(&mut self) {
|
||||
self.niri.unlock();
|
||||
self.niri.activate_monitors(&mut self.backend);
|
||||
self.niri
|
||||
.idle_notifier_state
|
||||
.notify_activity(&self.niri.seat);
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -277,11 +428,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();
|
||||
}
|
||||
@@ -296,6 +447,7 @@ impl SecurityContextHandler for State {
|
||||
compositor_state: Default::default(),
|
||||
can_view_decoration_globals: config.prefer_no_csd,
|
||||
restricted: true,
|
||||
credentials_unknown: false,
|
||||
});
|
||||
|
||||
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
|
||||
@@ -336,6 +488,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();
|
||||
}
|
||||
}
|
||||
@@ -349,12 +502,12 @@ 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;
|
||||
}
|
||||
|
||||
@@ -364,7 +517,7 @@ impl ForeignToplevelHandler for State {
|
||||
if &requested_output != current_output {
|
||||
self.niri
|
||||
.layout
|
||||
.move_window_to_output(&window, &requested_output);
|
||||
.move_to_output(Some(&window), &requested_output, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,25 +535,42 @@ 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 DrmLeaseHandler for State {
|
||||
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
|
||||
&mut self
|
||||
.backend
|
||||
self.backend
|
||||
.tty()
|
||||
.get_device_from_node(node)
|
||||
.unwrap()
|
||||
.drm_lease_state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn lease_request(
|
||||
@@ -471,3 +641,66 @@ impl GammaControlHandler for State {
|
||||
}
|
||||
}
|
||||
delegate_gamma_control!(State);
|
||||
|
||||
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 {
|
||||
// Only tokens that were created while the application has keyboard focus are valid.
|
||||
let Some((serial, seat)) = data.serial else {
|
||||
return false;
|
||||
};
|
||||
let Some(seat) = Seat::<State>::from_resource(&seat) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let keyboard = seat.get_keyboard().unwrap();
|
||||
keyboard
|
||||
.last_enter()
|
||||
.map(|last_enter| serial.is_no_older_than(&last_enter))
|
||||
.unwrap_or(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(&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();
|
||||
} 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);
|
||||
|
||||
+667
-156
File diff suppressed because it is too large
Load Diff
+1356
-186
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,234 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::desktop::Window;
|
||||
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::{IsAlive, Logical, Point};
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct MoveGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
window: Window,
|
||||
is_moving: bool,
|
||||
}
|
||||
|
||||
impl MoveGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
window,
|
||||
is_moving: false,
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
let ongoing = data.niri.layout.interactive_move_update(
|
||||
&self.window,
|
||||
event_delta,
|
||||
output,
|
||||
pos_within_output,
|
||||
);
|
||||
if ongoing {
|
||||
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||
if self.is_moving {
|
||||
data.niri.layout.view_offset_gesture_update(
|
||||
-event_delta.x,
|
||||
timestamp,
|
||||
false,
|
||||
);
|
||||
}
|
||||
// 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);
|
||||
|
||||
// MouseButton::Middle
|
||||
if event.button == 0x112 {
|
||||
if event.state == ButtonState::Pressed {
|
||||
let output = data
|
||||
.niri
|
||||
.output_under(handle.current_location())
|
||||
.map(|(output, _)| output)
|
||||
.cloned();
|
||||
// FIXME: workspace switch gesture.
|
||||
if let Some(output) = output {
|
||||
self.is_moving = true;
|
||||
data.niri.layout.view_offset_gesture_begin(&output, false);
|
||||
}
|
||||
} else if event.state == ButtonState::Released {
|
||||
self.is_moving = false;
|
||||
data.niri.layout.view_offset_gesture_end(false, None);
|
||||
}
|
||||
}
|
||||
|
||||
// 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,175 @@
|
||||
use smithay::desktop::Window;
|
||||
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::{IsAlive, Logical, Point};
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct ResizeGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
window: Window,
|
||||
}
|
||||
|
||||
impl ResizeGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
|
||||
Self { start_data, window }
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_resize_end(&self.window);
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for ResizeGrab {
|
||||
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() {
|
||||
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, 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,230 @@
|
||||
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::niri::State;
|
||||
|
||||
pub struct SpatialMovementGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
output: Output,
|
||||
gesture: GestureState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum GestureState {
|
||||
Recognizing,
|
||||
ViewOffset,
|
||||
WorkspaceSwitch,
|
||||
}
|
||||
|
||||
impl SpatialMovementGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>, output: Output) -> Self {
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
output,
|
||||
gesture: GestureState::Recognizing,
|
||||
}
|
||||
}
|
||||
|
||||
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(false, Some(false)),
|
||||
GestureState::WorkspaceSwitch => {
|
||||
layout.workspace_switch_gesture_end(false, 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;
|
||||
layout.view_offset_gesture_begin(&self.output, false);
|
||||
layout.view_offset_gesture_update(-c.x, timestamp, false)
|
||||
} 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 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<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,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);
|
||||
}
|
||||
}
|
||||
+478
-131
@@ -1,49 +1,118 @@
|
||||
use std::env;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::Shutdown;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::iter::Peekable;
|
||||
use std::slice;
|
||||
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use niri_ipc::{LogicalOutput, Mode, Output, Reply, Request, Response};
|
||||
use niri_config::OutputName;
|
||||
use niri_ipc::socket::Socket;
|
||||
use niri_ipc::{
|
||||
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response,
|
||||
Transform, Window,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::cli::Msg;
|
||||
use crate::utils::version;
|
||||
|
||||
pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
let socket_path = env::var_os(niri_ipc::SOCKET_PATH_ENV).with_context(|| {
|
||||
format!(
|
||||
"{} is not set, are you running this within niri?",
|
||||
niri_ipc::SOCKET_PATH_ENV
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut stream =
|
||||
UnixStream::connect(socket_path).context("error connecting to {socket_path}")?;
|
||||
|
||||
let request = match &msg {
|
||||
Msg::Version => Request::Version,
|
||||
Msg::Outputs => Request::Outputs,
|
||||
Msg::FocusedWindow => Request::FocusedWindow,
|
||||
Msg::FocusedOutput => Request::FocusedOutput,
|
||||
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,
|
||||
};
|
||||
let mut buf = serde_json::to_vec(&request).unwrap();
|
||||
stream
|
||||
.write_all(&buf)
|
||||
.context("error writing IPC request")?;
|
||||
stream
|
||||
.shutdown(Shutdown::Write)
|
||||
.context("error closing IPC stream for writing")?;
|
||||
|
||||
buf.clear();
|
||||
stream
|
||||
.read_to_end(&mut buf)
|
||||
.context("error reading IPC response")?;
|
||||
let socket = Socket::connect().context("error connecting to the niri socket")?;
|
||||
|
||||
let reply: Reply = serde_json::from_slice(&buf).context("error parsing IPC reply")?;
|
||||
let (reply, mut read_event) = socket
|
||||
.send(request)
|
||||
.context("error communicating with niri")?;
|
||||
|
||||
let response = reply
|
||||
.map_err(|msg| anyhow!(msg))
|
||||
.context("niri could not handle the 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()
|
||||
.map(|(reply, _read_event)| reply)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Default SIGPIPE so that our prints don't panic on stdout closing.
|
||||
unsafe {
|
||||
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.");
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
anyhow!(err_msg).context("niri returned an error")
|
||||
})?;
|
||||
|
||||
match msg {
|
||||
Msg::RequestError => {
|
||||
bail!("unexpected response: expected an error, got {response:?}");
|
||||
}
|
||||
Msg::Version => {
|
||||
let Response::Version(compositor_version) = response else {
|
||||
bail!("unexpected response: expected Version, got {response:?}");
|
||||
};
|
||||
|
||||
let cli_version = version();
|
||||
|
||||
if json {
|
||||
println!(
|
||||
"{}",
|
||||
json!({
|
||||
"compositor": compositor_version,
|
||||
"cli": cli_version,
|
||||
})
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if cli_version != compositor_version {
|
||||
eprintln!("Running niri compositor has a different version from the niri CLI.");
|
||||
eprintln!("Did you forget to restart niri after an update?");
|
||||
eprintln!();
|
||||
}
|
||||
|
||||
println!("Compositor version: {compositor_version}");
|
||||
println!("CLI version: {cli_version}");
|
||||
}
|
||||
Msg::Outputs => {
|
||||
let Response::Outputs(outputs) = response else {
|
||||
bail!("unexpected response: expected Outputs, got {response:?}");
|
||||
@@ -56,95 +125,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,
|
||||
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 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 {
|
||||
niri_ipc::Transform::Normal => "normal",
|
||||
niri_ipc::Transform::_90 => "90° counter-clockwise",
|
||||
niri_ipc::Transform::_180 => "180°",
|
||||
niri_ipc::Transform::_270 => "270° counter-clockwise",
|
||||
niri_ipc::Transform::Flipped => "flipped horizontally",
|
||||
niri_ipc::Transform::Flipped90 => {
|
||||
"90° counter-clockwise, flipped horizontally"
|
||||
}
|
||||
niri_ipc::Transform::Flipped180 => "flipped vertically",
|
||||
niri_ipc::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!();
|
||||
}
|
||||
}
|
||||
@@ -160,29 +148,388 @@ 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::Action { .. } => {
|
||||
let Response::Handled = response else {
|
||||
bail!("unexpected response: expected Handled, got {response:?}");
|
||||
};
|
||||
}
|
||||
Msg::Output { output, .. } => {
|
||||
let Response::OutputConfigChanged(response) = response else {
|
||||
bail!("unexpected response: expected OutputConfigChanged, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let response =
|
||||
serde_json::to_string(&response).context("error formatting response")?;
|
||||
println!("{response}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if response == OutputConfigChanged::OutputWasMissing {
|
||||
println!("Output \"{output}\" is not connected.");
|
||||
println!("The change will apply when it is connected.");
|
||||
}
|
||||
}
|
||||
Msg::Workspaces => {
|
||||
let Response::Workspaces(mut response) = response else {
|
||||
bail!("unexpected response: expected Workspaces, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let response =
|
||||
serde_json::to_string(&response).context("error formatting response")?;
|
||||
println!("{response}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if response.is_empty() {
|
||||
println!("No workspaces.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
response.sort_by_key(|ws| ws.idx);
|
||||
response.sort_by(|a, b| a.output.cmp(&b.output));
|
||||
|
||||
let mut current_output = if let Some(output) = response[0].output.as_deref() {
|
||||
println!("Output \"{output}\":");
|
||||
Some(output)
|
||||
} else {
|
||||
println!("No output:");
|
||||
None
|
||||
};
|
||||
|
||||
for ws in &response {
|
||||
if ws.output.as_deref() != current_output {
|
||||
let output = ws.output.as_deref().context(
|
||||
"invalid response: workspace with no output \
|
||||
following a workspace with an output",
|
||||
)?;
|
||||
current_output = Some(output);
|
||||
println!("\nOutput \"{output}\":");
|
||||
}
|
||||
|
||||
let is_active = if ws.is_active { " * " } else { " " };
|
||||
let idx = ws.idx;
|
||||
let name = if let Some(name) = ws.name.as_deref() {
|
||||
format!(" \"{name}\"")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
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.");
|
||||
}
|
||||
|
||||
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::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::KeyboardLayoutsChanged { keyboard_layouts } => {
|
||||
println!("Keyboard layouts changed: {keyboard_layouts:?}");
|
||||
}
|
||||
Event::KeyboardLayoutSwitched { idx } => {
|
||||
println!("Keyboard layout switched: {idx}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 { "" };
|
||||
println!("Window ID {}:{focused}", 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)");
|
||||
}
|
||||
}
|
||||
|
||||
+507
-36
@@ -1,32 +1,60 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
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::{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, Reply, Request, Response, Workspace};
|
||||
use smithay::desktop::layer_map_for_output;
|
||||
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::wayland::shell::wlr_layer::{KeyboardInteractivity, Layer};
|
||||
|
||||
use crate::backend::IpcOutputMap;
|
||||
use crate::layout::workspace::WorkspaceId;
|
||||
use crate::niri::State;
|
||||
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,
|
||||
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 {
|
||||
@@ -58,7 +86,34 @@ impl IpcServer {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Ok(Self { socket_path })
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for idx in to_remove.into_iter().rev() {
|
||||
let stream = streams.swap_remove(idx);
|
||||
let _ = stream.disconnect.send_blocking(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,10 +143,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 {
|
||||
@@ -104,7 +163,7 @@ 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();
|
||||
|
||||
@@ -114,53 +173,465 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow:
|
||||
.await
|
||||
.context("error reading request")?;
|
||||
|
||||
let reply = process(&ctx, &buf).map_err(|err| {
|
||||
warn!("error processing IPC request: {err:?}");
|
||||
err.to_string()
|
||||
});
|
||||
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 requested_event_stream = matches!(request, Ok(Request::EventStream));
|
||||
|
||||
let buf = serde_json::to_vec(&reply).context("error formatting reply")?;
|
||||
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:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let mut buf = serde_json::to_vec(&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);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process(ctx: &ClientCtx, buf: &str) -> anyhow::Result<Response> {
|
||||
let request: Request = serde_json::from_str(buf).context("error parsing request")?;
|
||||
|
||||
async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
let response = match request {
|
||||
Request::ReturnError => return Err(String::from("example compositor error")),
|
||||
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::Action(action) => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
|
||||
let action = niri_config::Action::from(action);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
state.do_action(action);
|
||||
// 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(());
|
||||
});
|
||||
|
||||
// Wait until the action has been processed before returning. This is important for a
|
||||
// few actions, for instance for DoScreenTransition this wait ensures that the screen
|
||||
// contents were sampled into the texture.
|
||||
let _ = rx.recv().await;
|
||||
Response::Handled
|
||||
}
|
||||
Request::Output { output, action } => {
|
||||
let ipc_outputs = ctx.ipc_outputs.lock().unwrap();
|
||||
let found = ipc_outputs
|
||||
.values()
|
||||
.any(|o| OutputName::from_ipc_output(o).matches(&output));
|
||||
let response = if found {
|
||||
OutputConfigChanged::Applied
|
||||
} else {
|
||||
OutputConfigChanged::OutputWasMissing
|
||||
};
|
||||
drop(ipc_outputs);
|
||||
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
state.apply_transient_output_config(&output, action);
|
||||
});
|
||||
|
||||
Response::OutputConfigChanged(response)
|
||||
}
|
||||
Request::FocusedOutput => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
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 output = result.map_err(|_| String::from("error getting active output info"))?;
|
||||
Response::FocusedOutput(output)
|
||||
}
|
||||
Request::EventStream => Response::Handled,
|
||||
};
|
||||
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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 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_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) });
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use niri_config::layer_rule::LayerRule;
|
||||
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, Rectangle, Scale};
|
||||
|
||||
use super::ResolvedLayerRules;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use crate::render_helpers::{RenderTarget, SplitElements};
|
||||
|
||||
#[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: RefCell<SolidColorBuffer>,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
LayerSurfaceRenderElement<R> => {
|
||||
Wayland = WaylandSurfaceRenderElement<R>,
|
||||
SolidColor = SolidColorRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
impl MappedLayer {
|
||||
pub fn new(surface: LayerSurface, rules: ResolvedLayerRules) -> Self {
|
||||
Self {
|
||||
surface,
|
||||
rules,
|
||||
block_out_buffer: RefCell::new(SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.])),
|
||||
}
|
||||
}
|
||||
|
||||
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 render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
geometry: Rectangle<i32, Logical>,
|
||||
scale: Scale<f64>,
|
||||
target: RenderTarget,
|
||||
) -> SplitElements<LayerSurfaceRenderElement<R>> {
|
||||
let mut rv = SplitElements::default();
|
||||
|
||||
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
|
||||
|
||||
if target.should_block_out(self.rules.block_out_from) {
|
||||
// Round to physical pixels.
|
||||
let geometry = geometry
|
||||
.to_f64()
|
||||
.to_physical_precise_round(scale)
|
||||
.to_logical(scale);
|
||||
|
||||
let mut buffer = self.block_out_buffer.borrow_mut();
|
||||
buffer.resize(geometry.size.to_f64());
|
||||
let elem = SolidColorRenderElement::from_buffer(
|
||||
&buffer,
|
||||
geometry.loc,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
rv.normal.push(elem.into());
|
||||
} else {
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let buf_pos = geometry.loc;
|
||||
|
||||
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_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,
|
||||
);
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use niri_config::layer_rule::{LayerRule, Match};
|
||||
use niri_config::BlockOutFrom;
|
||||
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 window with.
|
||||
pub opacity: Option<f32>,
|
||||
/// Whether to block out this window from certain render targets.
|
||||
pub block_out_from: Option<BlockOutFrom>,
|
||||
}
|
||||
|
||||
impl ResolvedLayerRules {
|
||||
pub const fn empty() -> Self {
|
||||
Self {
|
||||
opacity: None,
|
||||
block_out_from: None,
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use glam::{Mat3, Vec2};
|
||||
use niri_config::BlockOutFrom;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::utils::{
|
||||
Relocate, RelocateRenderElement, RescaleRenderElement,
|
||||
};
|
||||
use smithay::backend::renderer::element::{Kind, RenderElement};
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform};
|
||||
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;
|
||||
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.
|
||||
buffer: TextureBuffer<GlesTexture>,
|
||||
|
||||
/// Blocked-out contents of the window.
|
||||
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<f64, Logical>,
|
||||
|
||||
/// Position in the workspace.
|
||||
pos: Point<f64, Logical>,
|
||||
|
||||
/// How much the texture should be offset.
|
||||
buffer_offset: Point<f64, Logical>,
|
||||
|
||||
/// How much the blocked-out texture should be offset.
|
||||
blocked_out_buffer_offset: Point<f64, Logical>,
|
||||
|
||||
/// The closing animation.
|
||||
anim_state: AnimationState,
|
||||
|
||||
/// Random seed for the shader.
|
||||
random_seed: f32,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
ClosingWindowRenderElement => {
|
||||
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
|
||||
Shader = ShaderRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
#[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<f64, Logical>,
|
||||
pos: Point<f64, Logical>,
|
||||
blocker: TransactionBlocker,
|
||||
anim: Animation,
|
||||
) -> anyhow::Result<Self> {
|
||||
let _span = tracy_client::span!("ClosingWindow::new");
|
||||
|
||||
let mut render_to_texture = |elements: Vec<E>| -> anyhow::Result<_> {
|
||||
let (texture, _sync_point, geo) = render_to_encompassing_texture(
|
||||
renderer,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
Fourcc::Abgr8888,
|
||||
&elements,
|
||||
)
|
||||
.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((buffer, offset))
|
||||
};
|
||||
|
||||
let (buffer, buffer_offset) =
|
||||
render_to_texture(snapshot.contents).context("error rendering contents")?;
|
||||
let (blocked_out_buffer, blocked_out_buffer_offset) =
|
||||
render_to_texture(snapshot.blocked_out_contents)
|
||||
.context("error rendering blocked-out contents")?;
|
||||
|
||||
Ok(Self {
|
||||
buffer,
|
||||
blocked_out_buffer,
|
||||
block_out_from: snapshot.block_out_from,
|
||||
geo_size,
|
||||
pos,
|
||||
buffer_offset,
|
||||
blocked_out_buffer_offset,
|
||||
anim_state: AnimationState::new(blocker, anim),
|
||||
random_seed: fastrand::f32(),
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
match &self.anim_state {
|
||||
AnimationState::Waiting { .. } => true,
|
||||
AnimationState::Animating(anim) => !anim.is_done(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut GlesRenderer,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
target: RenderTarget,
|
||||
) -> ClosingWindowRenderElement {
|
||||
let (buffer, offset) = if target.should_block_out(self.block_out_from) {
|
||||
(&self.blocked_out_buffer, self.blocked_out_buffer_offset)
|
||||
} else {
|
||||
(&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);
|
||||
|
||||
// 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 = 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 = 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);
|
||||
|
||||
return ShaderRenderElement::new(
|
||||
ProgramType::Close,
|
||||
view_rect.size,
|
||||
None,
|
||||
scale.x as f32,
|
||||
1.,
|
||||
vec![
|
||||
mat3_uniform("niri_input_to_geo", input_to_geo),
|
||||
Uniform::new("niri_geo_size", geo_size.to_array()),
|
||||
mat3_uniform("niri_geo_to_tex", geo_to_tex),
|
||||
Uniform::new("niri_progress", progress as f32),
|
||||
Uniform::new("niri_clamped_progress", clamped_progress as f32),
|
||||
Uniform::new("niri_random_seed", self.random_seed),
|
||||
],
|
||||
HashMap::from([(String::from("niri_tex"), buffer.texture().clone())]),
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.with_location(Point::from((0., 0.)))
|
||||
.into();
|
||||
}
|
||||
|
||||
let elem = TextureRenderElement::from_texture_buffer(
|
||||
buffer.clone(),
|
||||
Point::from((0., 0.)),
|
||||
1. - clamped_progress as f32,
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
|
||||
let elem = PrimaryGpuTextureRenderElement(elem);
|
||||
|
||||
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 + offset;
|
||||
location.x -= view_rect.loc.x;
|
||||
let elem = RelocateRenderElement::from_element(
|
||||
elem,
|
||||
location.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
);
|
||||
|
||||
elem.into()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+189
-88
@@ -1,30 +1,31 @@
|
||||
use std::iter::zip;
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
use niri_config::GradientRelativeTo;
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use niri_config::{CornerRadius, Gradient, GradientInterpolation, GradientRelativeTo};
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::gradient::GradientRenderElement;
|
||||
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; 4],
|
||||
locations: [Point<i32, Logical>; 4],
|
||||
sizes: [Size<i32, Logical>; 4],
|
||||
full_size: Size<i32, Logical>,
|
||||
is_active: bool,
|
||||
buffers: [SolidColorBuffer; 8],
|
||||
locations: [Point<f64, Logical>; 8],
|
||||
sizes: [Size<f64, Logical>; 8],
|
||||
borders: [BorderRenderElement; 8],
|
||||
full_size: Size<f64, Logical>,
|
||||
is_border: bool,
|
||||
use_border_shader: bool,
|
||||
config: niri_config::FocusRing,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
FocusRingRenderElement => {
|
||||
SolidColor = SolidColorRenderElement,
|
||||
Gradient = GradientRenderElement,
|
||||
Gradient = BorderRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,9 +35,10 @@ impl FocusRing {
|
||||
buffers: Default::default(),
|
||||
locations: Default::default(),
|
||||
sizes: Default::default(),
|
||||
borders: Default::default(),
|
||||
full_size: Default::default(),
|
||||
is_active: false,
|
||||
is_border: false,
|
||||
use_border_shader: false,
|
||||
config,
|
||||
}
|
||||
}
|
||||
@@ -45,117 +47,216 @@ impl FocusRing {
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
pub fn update(&mut self, win_size: Size<i32, Logical>, is_border: bool) {
|
||||
let width = i32::from(self.config.width);
|
||||
self.full_size = win_size + Size::from((width * 2, width * 2));
|
||||
|
||||
if is_border {
|
||||
self.sizes[0] = Size::from((win_size.w + width * 2, width));
|
||||
self.sizes[1] = Size::from((win_size.w + width * 2, width));
|
||||
self.sizes[2] = Size::from((width, win_size.h));
|
||||
self.sizes[3] = Size::from((width, win_size.h));
|
||||
|
||||
for (buf, size) in zip(&mut self.buffers, self.sizes) {
|
||||
buf.resize(size);
|
||||
}
|
||||
|
||||
self.locations[0] = Point::from((-width, -width));
|
||||
self.locations[1] = Point::from((-width, win_size.h));
|
||||
self.locations[2] = Point::from((-width, 0));
|
||||
self.locations[3] = Point::from((win_size.w, 0));
|
||||
} else {
|
||||
self.sizes[0] = self.full_size;
|
||||
self.buffers[0].resize(self.sizes[0]);
|
||||
self.locations[0] = Point::from((-width, -width));
|
||||
pub fn update_shaders(&mut self) {
|
||||
for elem in &mut self.borders {
|
||||
elem.damage_all();
|
||||
}
|
||||
|
||||
self.is_border = is_border;
|
||||
}
|
||||
|
||||
pub fn set_active(&mut self, is_active: bool) {
|
||||
pub fn update_render_elements(
|
||||
&mut self,
|
||||
win_size: Size<f64, Logical>,
|
||||
is_active: bool,
|
||||
is_border: bool,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
radius: CornerRadius,
|
||||
scale: f64,
|
||||
) {
|
||||
let width = self.config.width.0;
|
||||
self.full_size = win_size + Size::from((width, width)).upscale(2.);
|
||||
|
||||
let color = if is_active {
|
||||
self.config.active_color.into()
|
||||
self.config.active_color
|
||||
} else {
|
||||
self.config.inactive_color.into()
|
||||
self.config.inactive_color
|
||||
};
|
||||
|
||||
for buf in &mut self.buffers {
|
||||
buf.set_color(color);
|
||||
buf.set_color(color.to_array_premul());
|
||||
}
|
||||
|
||||
self.is_active = is_active;
|
||||
}
|
||||
let radius = radius.fit_to(self.full_size.w as f32, self.full_size.h as f32);
|
||||
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<i32, Logical>,
|
||||
scale: Scale<f64>,
|
||||
view_size: Size<i32, Logical>,
|
||||
) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
let mut rv = ArrayVec::<_, 4>::new();
|
||||
|
||||
if self.config.off {
|
||||
return rv.into_iter();
|
||||
}
|
||||
|
||||
let gradient = if self.is_active {
|
||||
let gradient = if is_active {
|
||||
self.config.active_gradient
|
||||
} else {
|
||||
self.config.inactive_gradient
|
||||
};
|
||||
|
||||
let full_rect = Rectangle::from_loc_and_size(location + self.locations[0], self.full_size);
|
||||
let view_rect = Rectangle::from_loc_and_size((0, 0), view_size);
|
||||
self.use_border_shader = radius != CornerRadius::default() || gradient.is_some();
|
||||
|
||||
let mut push = |buffer, location: Point<i32, Logical>, size: Size<i32, Logical>| {
|
||||
let elem = gradient.and_then(|gradient| {
|
||||
let gradient_area = match gradient.relative_to {
|
||||
GradientRelativeTo::Window => full_rect,
|
||||
GradientRelativeTo::WorkspaceView => view_rect,
|
||||
};
|
||||
GradientRenderElement::new(
|
||||
renderer,
|
||||
scale,
|
||||
Rectangle::from_loc_and_size(location, size),
|
||||
gradient_area,
|
||||
gradient.from.into(),
|
||||
gradient.to.into(),
|
||||
// Set the defaults for solid color + rounded corners.
|
||||
let gradient = gradient.unwrap_or(Gradient {
|
||||
from: color,
|
||||
to: color,
|
||||
angle: 0,
|
||||
relative_to: GradientRelativeTo::Window,
|
||||
in_: GradientInterpolation::default(),
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
let rounded_corner_border_width = if self.is_border {
|
||||
// HACK: increase the border width used for the inner rounded corners a tiny bit to
|
||||
// reduce background bleed.
|
||||
width as f32 + 0.5
|
||||
} else {
|
||||
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 = f64::max(width, ceil(f64::from(radius.top_left)));
|
||||
let top_right = f64::min(
|
||||
self.full_size.w - top_left,
|
||||
f64::max(width, ceil(f64::from(radius.top_right))),
|
||||
);
|
||||
let bottom_left = f64::min(
|
||||
self.full_size.h - top_left,
|
||||
f64::max(width, ceil(f64::from(radius.bottom_left))),
|
||||
);
|
||||
let bottom_right = f64::min(
|
||||
self.full_size.h - top_right,
|
||||
f64::min(
|
||||
self.full_size.w - bottom_left,
|
||||
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.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));
|
||||
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.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.locations[3] = Point::from((win_size.w, -width + top_right));
|
||||
|
||||
// Top-left corner.
|
||||
self.sizes[4] = Size::from((top_left, top_left));
|
||||
self.locations[4] = Point::from((-width, -width));
|
||||
|
||||
// Top-right corner.
|
||||
self.sizes[5] = Size::from((top_right, top_right));
|
||||
self.locations[5] = Point::from((win_size.w + width - top_right, -width));
|
||||
|
||||
// Bottom-right corner.
|
||||
self.sizes[6] = Size::from((bottom_right, bottom_right));
|
||||
self.locations[6] = Point::from((
|
||||
win_size.w + width - bottom_right,
|
||||
win_size.h + width - bottom_right,
|
||||
));
|
||||
|
||||
// Bottom-left corner.
|
||||
self.sizes[7] = Size::from((bottom_left, bottom_left));
|
||||
self.locations[7] = Point::from((-width, win_size.h + width - bottom_left));
|
||||
|
||||
for (buf, size) in zip(&mut self.buffers, self.sizes) {
|
||||
buf.resize(size);
|
||||
}
|
||||
|
||||
for (border, (loc, size)) in zip(&mut self.borders, zip(self.locations, self.sizes)) {
|
||||
border.update(
|
||||
size,
|
||||
Rectangle::new(gradient_area.loc - loc, gradient_area.size),
|
||||
gradient.in_,
|
||||
gradient.from,
|
||||
gradient.to,
|
||||
((gradient.angle as f32) - 90.).to_radians(),
|
||||
)
|
||||
.map(Into::into)
|
||||
});
|
||||
Rectangle::new(full_rect.loc - loc, full_rect.size),
|
||||
rounded_corner_border_width,
|
||||
radius,
|
||||
scale as f32,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.sizes[0] = self.full_size;
|
||||
self.buffers[0].resize(self.sizes[0]);
|
||||
self.locations[0] = Point::from((-width, -width));
|
||||
|
||||
let elem = elem.unwrap_or_else(|| {
|
||||
SolidColorRenderElement::from_buffer(
|
||||
buffer,
|
||||
location.to_physical_precise_round(scale),
|
||||
scale,
|
||||
1.,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into()
|
||||
});
|
||||
self.borders[0].update(
|
||||
self.sizes[0],
|
||||
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::new(full_rect.loc - self.locations[0], full_rect.size),
|
||||
rounded_corner_border_width,
|
||||
radius,
|
||||
scale as f32,
|
||||
);
|
||||
}
|
||||
|
||||
self.is_border = is_border;
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
let mut rv = ArrayVec::<_, 8>::new();
|
||||
|
||||
if self.config.off {
|
||||
return rv.into_iter();
|
||||
}
|
||||
|
||||
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. {
|
||||
return rv.into_iter();
|
||||
}
|
||||
|
||||
let has_border_shader = BorderRenderElement::has_shader(renderer);
|
||||
|
||||
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, 1., Kind::Unspecified).into()
|
||||
};
|
||||
rv.push(elem);
|
||||
};
|
||||
|
||||
if self.is_border {
|
||||
for (buf, (loc, size)) in zip(&self.buffers, zip(self.locations, self.sizes)) {
|
||||
push(buf, location + loc, size);
|
||||
for ((buf, border), loc) in zip(zip(&self.buffers, &self.borders), self.locations) {
|
||||
push(buf, border, location + loc);
|
||||
}
|
||||
} else {
|
||||
push(
|
||||
&self.buffers[0],
|
||||
&self.borders[0],
|
||||
location + self.locations[0],
|
||||
self.sizes[0],
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
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,
|
||||
active_gradient: config.gradient,
|
||||
inactive_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,
|
||||
active_gradient: config.gradient,
|
||||
inactive_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, view_rect, radius, scale);
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
self.inner.render(renderer, location)
|
||||
}
|
||||
}
|
||||
+4772
-676
File diff suppressed because it is too large
Load Diff
+576
-338
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,151 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use glam::{Mat3, Vec2};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::utils::{
|
||||
Relocate, RelocateRenderElement, RescaleRenderElement,
|
||||
};
|
||||
use smithay::backend::renderer::element::{Kind, RenderElement};
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, Uniform};
|
||||
use smithay::backend::renderer::Texture;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
|
||||
|
||||
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::shader_element::ShaderRenderElement;
|
||||
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
|
||||
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OpenAnimation {
|
||||
anim: Animation,
|
||||
random_seed: f32,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
OpeningWindowRenderElement => {
|
||||
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
|
||||
Shader = ShaderRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenAnimation {
|
||||
pub fn new(anim: Animation) -> Self {
|
||||
Self {
|
||||
anim,
|
||||
random_seed: fastrand::f32(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self) {}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.anim.is_done()
|
||||
}
|
||||
|
||||
// We can't depend on view_rect here, because the result of window opening can be snapshot and
|
||||
// then rendered elsewhere.
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut GlesRenderer,
|
||||
elements: &[impl RenderElement<GlesRenderer>],
|
||||
geo_size: Size<f64, Logical>,
|
||||
location: Point<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
) -> anyhow::Result<OpeningWindowRenderElement> {
|
||||
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);
|
||||
|
||||
if Shaders::get(renderer).program(ProgramType::Open).is_some() {
|
||||
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()).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_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);
|
||||
|
||||
let geo_loc = Vec2::new(location.x as f32, location.y as f32);
|
||||
let geo_size = Vec2::new(geo_size.w as f32, 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(scale.x as f32, 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 geo_to_tex =
|
||||
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
|
||||
|
||||
return Ok(ShaderRenderElement::new(
|
||||
ProgramType::Open,
|
||||
area.size,
|
||||
None,
|
||||
scale.x as f32,
|
||||
1.,
|
||||
vec![
|
||||
mat3_uniform("niri_input_to_geo", input_to_geo),
|
||||
Uniform::new("niri_geo_size", geo_size.to_array()),
|
||||
mat3_uniform("niri_geo_to_tex", geo_to_tex),
|
||||
Uniform::new("niri_progress", progress as f32),
|
||||
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())]),
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.with_location(area.loc)
|
||||
.into());
|
||||
}
|
||||
|
||||
let buffer =
|
||||
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, Vec::new());
|
||||
let elem = TextureRenderElement::from_texture_buffer(
|
||||
buffer,
|
||||
Point::from((0., 0.)),
|
||||
clamped_progress as f32,
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
|
||||
let elem = PrimaryGpuTextureRenderElement(elem);
|
||||
|
||||
let center = geo_size.to_point().downscale(2.);
|
||||
let elem = RescaleRenderElement::from_element(
|
||||
elem,
|
||||
(center - offset).to_physical_precise_round(scale),
|
||||
(progress / 2. + 0.5).max(0.),
|
||||
);
|
||||
|
||||
let elem = RelocateRenderElement::from_element(
|
||||
elem,
|
||||
(location + offset).to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
);
|
||||
|
||||
Ok(elem.into())
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+773
-172
File diff suppressed because it is too large
Load Diff
+1387
-1876
File diff suppressed because it is too large
Load Diff
+4
-2
@@ -11,13 +11,12 @@ pub mod frame_clock;
|
||||
pub mod handlers;
|
||||
pub mod input;
|
||||
pub mod ipc;
|
||||
pub mod layer;
|
||||
pub mod layout;
|
||||
pub mod niri;
|
||||
pub mod protocols;
|
||||
pub mod render_helpers;
|
||||
pub mod rubber_band;
|
||||
pub mod scroll_tracker;
|
||||
pub mod swipe_tracker;
|
||||
pub mod ui;
|
||||
pub mod utils;
|
||||
pub mod window;
|
||||
@@ -29,3 +28,6 @@ pub mod pw_utils;
|
||||
|
||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||
pub use dummy_pw_utils as pw_utils;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
+108
-73
@@ -11,24 +11,32 @@ use std::{env, mem};
|
||||
|
||||
use clap::Parser;
|
||||
use directories::ProjectDirs;
|
||||
use niri::animation;
|
||||
use niri::cli::{Cli, Sub};
|
||||
#[cfg(feature = "dbus")]
|
||||
use niri::dbus;
|
||||
use niri::ipc::client::handle_msg;
|
||||
use niri::niri::State;
|
||||
use niri::utils::spawning::{
|
||||
spawn, CHILD_ENV, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE,
|
||||
spawn, store_and_increase_nofile_rlimit, CHILD_ENV, REMOVE_ENV_RUST_BACKTRACE,
|
||||
REMOVE_ENV_RUST_LIB_BACKTRACE,
|
||||
};
|
||||
use niri::utils::watcher::Watcher;
|
||||
use niri::utils::{cause_panic, version, IS_SYSTEMD_SERVICE};
|
||||
use niri_config::Config;
|
||||
use niri_ipc::socket::SOCKET_PATH_ENV;
|
||||
use portable_atomic::Ordering;
|
||||
use sd_notify::NotifyState;
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
use smithay::reexports::wayland_server::Display;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
const DEFAULT_LOG_FILTER: &str = "niri=debug,smithay::backend::renderer::gles=error";
|
||||
|
||||
#[cfg(feature = "profile-with-tracy-allocations")]
|
||||
#[global_allocator]
|
||||
static GLOBAL: tracy_client::ProfiledAllocator<std::alloc::System> =
|
||||
tracy_client::ProfiledAllocator::new(std::alloc::System, 100);
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Set backtrace defaults if not set.
|
||||
if env::var_os("RUST_BACKTRACE").is_none() {
|
||||
@@ -50,7 +58,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
);
|
||||
}
|
||||
|
||||
let directives = env::var("RUST_LOG").unwrap_or_else(|_| "niri=debug".to_owned());
|
||||
let directives = env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_FILTER.to_owned());
|
||||
let env_filter = EnvFilter::builder().parse_lossy(directives);
|
||||
tracing_subscriber::fmt()
|
||||
.compact()
|
||||
@@ -78,8 +86,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env::set_var("XDG_SESSION_TYPE", "wayland");
|
||||
}
|
||||
|
||||
let _client = tracy_client::Client::start();
|
||||
|
||||
// Set a better error printer for config loading.
|
||||
niri_config::set_miette_hook().unwrap();
|
||||
|
||||
@@ -87,9 +93,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let Some(subcommand) = cli.subcommand {
|
||||
match subcommand {
|
||||
Sub::Validate { config } => {
|
||||
let path = config
|
||||
.or_else(default_config_path)
|
||||
.expect("error getting config path");
|
||||
tracy_client::Client::start();
|
||||
|
||||
let (path, _, _) = config_path(config);
|
||||
Config::load(&path)?;
|
||||
info!("config is valid");
|
||||
return Ok(());
|
||||
@@ -102,71 +108,66 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid starting Tracy for the `niri msg` code path since starting/stopping Tracy is a bit
|
||||
// slow.
|
||||
tracy_client::Client::start();
|
||||
|
||||
info!("starting version {}", &version());
|
||||
|
||||
// Load the config.
|
||||
let mut config_created = false;
|
||||
let path = cli.config.or_else(|| {
|
||||
let default_path = default_config_path()?;
|
||||
let default_parent = default_path.parent().unwrap();
|
||||
let (path, watch_path, create_default) = config_path(cli.config);
|
||||
env::remove_var("NIRI_CONFIG");
|
||||
if create_default {
|
||||
let default_parent = path.parent().unwrap();
|
||||
|
||||
if let Err(err) = fs::create_dir_all(default_parent) {
|
||||
warn!(
|
||||
"error creating config directories {:?}: {err:?}",
|
||||
default_parent
|
||||
);
|
||||
return Some(default_path);
|
||||
}
|
||||
|
||||
// Create the config and fill it with the default config if it doesn't exist.
|
||||
let new_file = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&default_path);
|
||||
match new_file {
|
||||
Ok(mut new_file) => {
|
||||
let default = include_bytes!("../resources/default-config.kdl");
|
||||
match new_file.write_all(default) {
|
||||
Ok(()) => {
|
||||
config_created = true;
|
||||
info!("wrote default config to {:?}", &default_path);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error writing config file at {:?}: {err:?}", &default_path)
|
||||
match fs::create_dir_all(default_parent) {
|
||||
Ok(()) => {
|
||||
// Create the config and fill it with the default config if it doesn't exist.
|
||||
let new_file = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&path);
|
||||
match new_file {
|
||||
Ok(mut new_file) => {
|
||||
let default = include_bytes!("../resources/default-config.kdl");
|
||||
match new_file.write_all(default) {
|
||||
Ok(()) => {
|
||||
config_created = true;
|
||||
info!("wrote default config to {:?}", &path);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error writing config file at {:?}: {err:?}", &path)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
|
||||
Err(err) => warn!("error creating config file at {:?}: {err:?}", &path),
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
|
||||
Err(err) => warn!("error creating config file at {:?}: {err:?}", &default_path),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"error creating config directories {:?}: {err:?}",
|
||||
default_parent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some(default_path)
|
||||
});
|
||||
}
|
||||
|
||||
let mut config_errored = false;
|
||||
let mut config = path
|
||||
.as_deref()
|
||||
.and_then(|path| match Config::load(path) {
|
||||
Ok(config) => Some(config),
|
||||
Err(err) => {
|
||||
warn!("{err:?}");
|
||||
config_errored = true;
|
||||
None
|
||||
}
|
||||
let mut config = Config::load(&path)
|
||||
.map_err(|err| {
|
||||
warn!("{err:?}");
|
||||
config_errored = true;
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let slowdown = if config.animations.off {
|
||||
0.
|
||||
} else {
|
||||
config.animations.slowdown.clamp(0., 100.)
|
||||
};
|
||||
animation::ANIMATION_SLOWDOWN.store(slowdown, Ordering::Relaxed);
|
||||
|
||||
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
|
||||
*CHILD_ENV.write().unwrap() = mem::take(&mut config.environment);
|
||||
|
||||
store_and_increase_nofile_rlimit();
|
||||
|
||||
// Create the compositor.
|
||||
let mut event_loop = EventLoop::try_new().unwrap();
|
||||
let display = Display::new().unwrap();
|
||||
@@ -175,6 +176,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
event_loop.handle(),
|
||||
event_loop.get_signal(),
|
||||
display,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -188,7 +190,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Set NIRI_SOCKET for children.
|
||||
if let Some(ipc) = &state.niri.ipc_server {
|
||||
env::set_var(niri_ipc::SOCKET_PATH_ENV, &ipc.socket_path);
|
||||
env::set_var(SOCKET_PATH_ENV, &ipc.socket_path);
|
||||
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
|
||||
}
|
||||
|
||||
@@ -208,37 +210,37 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(feature = "dbus")]
|
||||
dbus::DBusServers::start(&mut state, cli.session);
|
||||
|
||||
// Notify systemd we're ready.
|
||||
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
|
||||
warn!("error notifying systemd: {err:?}");
|
||||
};
|
||||
if env::var_os("NIRI_DISABLE_SYSTEM_MANAGER_NOTIFY").map_or(true, |x| x != "1") {
|
||||
// Notify systemd we're ready.
|
||||
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
|
||||
warn!("error notifying systemd: {err:?}");
|
||||
};
|
||||
|
||||
// Send ready notification to the NOTIFY_FD file descriptor.
|
||||
if let Err(err) = notify_fd() {
|
||||
warn!("error notifying fd: {err:?}");
|
||||
// Send ready notification to the NOTIFY_FD file descriptor.
|
||||
if let Err(err) = notify_fd() {
|
||||
warn!("error notifying fd: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
// Set up config file watcher.
|
||||
let _watcher = if let Some(path) = path.clone() {
|
||||
let _watcher = {
|
||||
let (tx, rx) = calloop::channel::sync_channel(1);
|
||||
let watcher = Watcher::new(path.clone(), tx);
|
||||
let watcher = Watcher::new(watch_path.clone(), tx);
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(rx, move |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(()) => state.reload_config(path.clone()),
|
||||
calloop::channel::Event::Msg(()) => state.reload_config(watch_path.clone()),
|
||||
calloop::channel::Event::Closed => (),
|
||||
})
|
||||
.unwrap();
|
||||
Some(watcher)
|
||||
} else {
|
||||
None
|
||||
watcher
|
||||
};
|
||||
|
||||
// Spawn commands from cli and auto-start.
|
||||
spawn(cli.command);
|
||||
spawn(cli.command, None);
|
||||
|
||||
for elem in spawn_at_startup {
|
||||
spawn(elem.command);
|
||||
spawn(elem.command, None);
|
||||
}
|
||||
|
||||
// Show the config error notification right away if needed.
|
||||
@@ -261,7 +263,7 @@ fn import_environment() {
|
||||
"WAYLAND_DISPLAY",
|
||||
"XDG_CURRENT_DESKTOP",
|
||||
"XDG_SESSION_TYPE",
|
||||
niri_ipc::SOCKET_PATH_ENV,
|
||||
SOCKET_PATH_ENV,
|
||||
]
|
||||
.join(" ");
|
||||
|
||||
@@ -306,6 +308,12 @@ fn import_environment() {
|
||||
}
|
||||
}
|
||||
|
||||
fn env_config_path() -> Option<PathBuf> {
|
||||
env::var_os("NIRI_CONFIG")
|
||||
.filter(|x| !x.is_empty())
|
||||
.map(PathBuf::from)
|
||||
}
|
||||
|
||||
fn default_config_path() -> Option<PathBuf> {
|
||||
let Some(dirs) = ProjectDirs::from("", "", "niri") else {
|
||||
warn!("error retrieving home directory");
|
||||
@@ -317,6 +325,33 @@ fn default_config_path() -> Option<PathBuf> {
|
||||
Some(path)
|
||||
}
|
||||
|
||||
fn system_config_path() -> PathBuf {
|
||||
PathBuf::from("/etc/niri/config.kdl")
|
||||
}
|
||||
|
||||
/// Resolves and returns the config path to load, the config path to watch, and whether to create
|
||||
/// the default config at the path to load.
|
||||
fn config_path(cli_path: Option<PathBuf>) -> (PathBuf, PathBuf, bool) {
|
||||
if let Some(explicit) = cli_path.or_else(env_config_path) {
|
||||
return (explicit.clone(), explicit, false);
|
||||
}
|
||||
|
||||
let system_path = system_config_path();
|
||||
if let Some(path) = default_config_path() {
|
||||
if path.exists() {
|
||||
return (path.clone(), path, true);
|
||||
}
|
||||
|
||||
if system_path.exists() {
|
||||
(system_path, path, false)
|
||||
} else {
|
||||
(path.clone(), path, true)
|
||||
}
|
||||
} else {
|
||||
(system_path.clone(), system_path, false)
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_fd() -> anyhow::Result<()> {
|
||||
let fd = match env::var("NOTIFY_FD") {
|
||||
Ok(notify_fd) => notify_fd.parse()?,
|
||||
|
||||
+2108
-554
File diff suppressed because it is too large
Load Diff
@@ -11,10 +11,7 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::{
|
||||
ToplevelStateSet, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
|
||||
};
|
||||
use smithay::wayland::shell::xdg::{ToplevelStateSet, XdgToplevelSurfaceRoleAttributes};
|
||||
use wayland_protocols_wlr::foreign_toplevel::v1::server::{
|
||||
zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
|
||||
};
|
||||
@@ -22,6 +19,7 @@ use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
|
||||
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::utils::with_toplevel_role;
|
||||
|
||||
const VERSION: u32 = 3;
|
||||
|
||||
@@ -95,38 +93,24 @@ pub fn refresh(state: &mut State) {
|
||||
// Save the focused window for last, this way when the focus changes, we will first deactivate
|
||||
// the previous window and only then activate the newly focused window.
|
||||
let mut focused = None;
|
||||
state.niri.layout.with_windows(|mapped, output| {
|
||||
let wl_surface = mapped.toplevel().wl_surface();
|
||||
|
||||
with_states(wl_surface, |states| {
|
||||
let role = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
state.niri.layout.with_windows(|mapped, output, _| {
|
||||
let toplevel = mapped.toplevel();
|
||||
let wl_surface = toplevel.wl_surface();
|
||||
with_toplevel_role(toplevel, |role| {
|
||||
if state.niri.keyboard_focus.surface() == Some(wl_surface) {
|
||||
focused = Some((mapped.window.clone(), output.cloned()));
|
||||
} else {
|
||||
refresh_toplevel(protocol_state, wl_surface, &role, output, false);
|
||||
refresh_toplevel(protocol_state, wl_surface, role, output, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Finally, refresh the focused window.
|
||||
if let Some((window, output)) = focused {
|
||||
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();
|
||||
|
||||
refresh_toplevel(protocol_state, wl_surface, &role, output.as_ref(), true);
|
||||
let toplevel = window.toplevel().expect("no X11 support");
|
||||
let wl_surface = toplevel.wl_surface();
|
||||
with_toplevel_role(toplevel, |role| {
|
||||
refresh_toplevel(protocol_state, wl_surface, role, output.as_ref(), true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,6 @@ where
|
||||
}
|
||||
|
||||
// Verify that there's no more data.
|
||||
#[allow(clippy::unused_io_amount)] // False positive on 1.77.0
|
||||
{
|
||||
match file.read(&mut [0]) {
|
||||
Ok(0) => (),
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
pub mod foreign_toplevel;
|
||||
pub mod gamma_control;
|
||||
pub mod mutter_x11_interop;
|
||||
pub mod output_management;
|
||||
pub mod screencopy;
|
||||
|
||||
pub mod raw;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
use mutter_x11_interop::MutterX11Interop;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
|
||||
use super::raw::mutter_x11_interop::v1::server::mutter_x11_interop;
|
||||
|
||||
const VERSION: u32 = 1;
|
||||
|
||||
pub struct MutterX11InteropManagerState {}
|
||||
|
||||
pub struct MutterX11InteropManagerGlobalData {
|
||||
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||
}
|
||||
|
||||
pub trait MutterX11InteropHandler {}
|
||||
|
||||
impl MutterX11InteropManagerState {
|
||||
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
|
||||
where
|
||||
D: GlobalDispatch<MutterX11Interop, MutterX11InteropManagerGlobalData>,
|
||||
D: Dispatch<MutterX11Interop, ()>,
|
||||
D: MutterX11InteropHandler,
|
||||
D: 'static,
|
||||
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
|
||||
{
|
||||
let global_data = MutterX11InteropManagerGlobalData {
|
||||
filter: Box::new(filter),
|
||||
};
|
||||
display.create_global::<D, MutterX11Interop, _>(VERSION, global_data);
|
||||
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> GlobalDispatch<MutterX11Interop, MutterX11InteropManagerGlobalData, D>
|
||||
for MutterX11InteropManagerState
|
||||
where
|
||||
D: GlobalDispatch<MutterX11Interop, MutterX11InteropManagerGlobalData>,
|
||||
D: Dispatch<MutterX11Interop, ()>,
|
||||
D: MutterX11InteropHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn bind(
|
||||
_state: &mut D,
|
||||
_handle: &DisplayHandle,
|
||||
_client: &Client,
|
||||
manager: New<MutterX11Interop>,
|
||||
_manager_state: &MutterX11InteropManagerGlobalData,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
data_init.init(manager, ());
|
||||
}
|
||||
|
||||
fn can_view(client: Client, global_data: &MutterX11InteropManagerGlobalData) -> bool {
|
||||
(global_data.filter)(&client)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<MutterX11Interop, (), D> for MutterX11InteropManagerState
|
||||
where
|
||||
D: Dispatch<MutterX11Interop, ()>,
|
||||
D: MutterX11InteropHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
_state: &mut D,
|
||||
_client: &Client,
|
||||
_resource: &MutterX11Interop,
|
||||
request: <MutterX11Interop as Resource>::Request,
|
||||
_data: &(),
|
||||
_dhandle: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
match request {
|
||||
mutter_x11_interop::Request::Destroy => (),
|
||||
mutter_x11_interop::Request::SetX11Parent { .. } => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_mutter_x11_interop {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
$crate::protocols::raw::mutter_x11_interop::v1::server::mutter_x11_interop::MutterX11Interop: $crate::protocols::mutter_x11_interop::MutterX11InteropManagerGlobalData
|
||||
] => $crate::protocols::mutter_x11_interop::MutterX11InteropManagerState);
|
||||
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
$crate::protocols::raw::mutter_x11_interop::v1::server::mutter_x11_interop::MutterX11Interop: ()
|
||||
] => $crate::protocols::mutter_x11_interop::MutterX11InteropManagerState);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,897 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::iter::zip;
|
||||
use std::mem;
|
||||
|
||||
use niri_config::{FloatOrInt, OutputName, Vrr};
|
||||
use niri_ipc::Transform;
|
||||
use smithay::reexports::wayland_protocols_wlr::output_management::v1::server::{
|
||||
zwlr_output_configuration_head_v1, zwlr_output_configuration_v1, zwlr_output_head_v1,
|
||||
zwlr_output_manager_v1, zwlr_output_mode_v1,
|
||||
};
|
||||
use smithay::reexports::wayland_server::backend::ClientId;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::Transform as WlTransform;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum,
|
||||
};
|
||||
use zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1;
|
||||
use zwlr_output_configuration_v1::ZwlrOutputConfigurationV1;
|
||||
use zwlr_output_head_v1::{AdaptiveSyncState, ZwlrOutputHeadV1};
|
||||
use zwlr_output_manager_v1::ZwlrOutputManagerV1;
|
||||
use zwlr_output_mode_v1::ZwlrOutputModeV1;
|
||||
|
||||
use crate::backend::OutputId;
|
||||
use crate::niri::State;
|
||||
use crate::utils::ipc_transform_to_smithay;
|
||||
|
||||
const VERSION: u32 = 4;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ClientData {
|
||||
heads: HashMap<OutputId, (ZwlrOutputHeadV1, Vec<ZwlrOutputModeV1>)>,
|
||||
confs: HashMap<ZwlrOutputConfigurationV1, OutputConfigurationState>,
|
||||
manager: ZwlrOutputManagerV1,
|
||||
}
|
||||
|
||||
pub struct OutputManagementManagerState {
|
||||
display: DisplayHandle,
|
||||
serial: u32,
|
||||
clients: HashMap<ClientId, ClientData>,
|
||||
current_state: HashMap<OutputId, niri_ipc::Output>,
|
||||
current_config: niri_config::Outputs,
|
||||
}
|
||||
|
||||
pub struct OutputManagementManagerGlobalData {
|
||||
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||
}
|
||||
|
||||
pub trait OutputManagementHandler {
|
||||
fn output_management_state(&mut self) -> &mut OutputManagementManagerState;
|
||||
fn apply_output_config(&mut self, config: niri_config::Outputs);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum OutputConfigurationState {
|
||||
Ongoing(HashMap<OutputId, niri_config::Output>),
|
||||
Finished,
|
||||
}
|
||||
|
||||
pub enum OutputConfigurationHeadState {
|
||||
Cancelled,
|
||||
Ok(OutputId, ZwlrOutputConfigurationV1),
|
||||
}
|
||||
|
||||
impl OutputManagementManagerState {
|
||||
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
|
||||
where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
|
||||
{
|
||||
let global_data = OutputManagementManagerGlobalData {
|
||||
filter: Box::new(filter),
|
||||
};
|
||||
display.create_global::<D, ZwlrOutputManagerV1, _>(VERSION, global_data);
|
||||
|
||||
Self {
|
||||
display: display.clone(),
|
||||
clients: HashMap::new(),
|
||||
serial: 0,
|
||||
current_state: HashMap::new(),
|
||||
current_config: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_config_changed(&mut self, new_config: niri_config::Outputs) {
|
||||
self.current_config = new_config;
|
||||
}
|
||||
|
||||
pub fn notify_changes(&mut self, new_state: HashMap<OutputId, niri_ipc::Output>) {
|
||||
let mut changed = false; /* most likely to end up true */
|
||||
for (output, conf) in new_state.iter() {
|
||||
if let Some(old) = self.current_state.get(output) {
|
||||
if old.vrr_enabled != conf.vrr_enabled {
|
||||
changed = true;
|
||||
for client in self.clients.values() {
|
||||
if let Some((head, _)) = client.heads.get(output) {
|
||||
if head.version() >= zwlr_output_head_v1::EVT_ADAPTIVE_SYNC_SINCE {
|
||||
head.adaptive_sync(match conf.vrr_enabled {
|
||||
true => AdaptiveSyncState::Enabled,
|
||||
false => AdaptiveSyncState::Disabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TTY outputs can't change modes I think, however, winit and virtual outputs can.
|
||||
let modes_changed = old.modes != conf.modes;
|
||||
if modes_changed {
|
||||
changed = true;
|
||||
if old.modes.len() != conf.modes.len() {
|
||||
error!("output's old mode count doesn't match new modes");
|
||||
} else {
|
||||
for client in self.clients.values() {
|
||||
if let Some((_, modes)) = client.heads.get(output) {
|
||||
for (wl_mode, mode) in zip(modes, &conf.modes) {
|
||||
wl_mode.size(i32::from(mode.width), i32::from(mode.height));
|
||||
if let Ok(refresh_rate) = mode.refresh_rate.try_into() {
|
||||
wl_mode.refresh(refresh_rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match (old.current_mode, conf.current_mode) {
|
||||
(Some(old_index), Some(new_index)) => {
|
||||
if old.modes.len() == conf.modes.len()
|
||||
&& (modes_changed || old_index != new_index)
|
||||
{
|
||||
changed = true;
|
||||
for client in self.clients.values() {
|
||||
if let Some((head, modes)) = client.heads.get(output) {
|
||||
if let Some(new_mode) = modes.get(new_index) {
|
||||
head.current_mode(new_mode);
|
||||
} else {
|
||||
error!(
|
||||
"output new mode doesnt exist for the client's output"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(Some(_), None) => {
|
||||
changed = true;
|
||||
for client in self.clients.values() {
|
||||
if let Some((head, _)) = client.heads.get(output) {
|
||||
head.enabled(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, Some(new_index)) => {
|
||||
if old.modes.len() == conf.modes.len() {
|
||||
changed = true;
|
||||
for client in self.clients.values() {
|
||||
if let Some((head, modes)) = client.heads.get(output) {
|
||||
head.enabled(1);
|
||||
if let Some(mode) = modes.get(new_index) {
|
||||
head.current_mode(mode);
|
||||
} else {
|
||||
error!(
|
||||
"output new mode doesnt exist for the client's output"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
match (old.logical, conf.logical) {
|
||||
(Some(old_logical), Some(new_logical)) => {
|
||||
if old_logical != new_logical {
|
||||
changed = true;
|
||||
for client in self.clients.values() {
|
||||
if let Some((head, _)) = client.heads.get(output) {
|
||||
if old_logical.x != new_logical.x
|
||||
|| old_logical.y != new_logical.y
|
||||
{
|
||||
head.position(new_logical.x, new_logical.y);
|
||||
}
|
||||
if old_logical.scale != new_logical.scale {
|
||||
head.scale(new_logical.scale);
|
||||
}
|
||||
if old_logical.transform != new_logical.transform {
|
||||
head.transform(
|
||||
ipc_transform_to_smithay(new_logical.transform).into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, Some(new_logical)) => {
|
||||
changed = true;
|
||||
for client in self.clients.values() {
|
||||
if let Some((head, _)) = client.heads.get(output) {
|
||||
// head enable in the mode diff check
|
||||
head.position(new_logical.x, new_logical.y);
|
||||
head.transform(
|
||||
ipc_transform_to_smithay(new_logical.transform).into(),
|
||||
);
|
||||
head.scale(new_logical.scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
(Some(_), None) => {
|
||||
// heads disabled in the mode diff check
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
} else {
|
||||
changed = true;
|
||||
notify_new_head(self, output, conf);
|
||||
}
|
||||
}
|
||||
for (old, _) in self.current_state.iter() {
|
||||
if !new_state.contains_key(old) {
|
||||
changed = true;
|
||||
notify_removed_head(&mut self.clients, old);
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
self.current_state = new_state;
|
||||
self.serial += 1;
|
||||
for data in self.clients.values() {
|
||||
data.manager.done(self.serial);
|
||||
for conf in data.confs.keys() {
|
||||
conf.cancelled();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData, D>
|
||||
for OutputManagementManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn bind(
|
||||
state: &mut D,
|
||||
display: &DisplayHandle,
|
||||
client: &Client,
|
||||
manager: New<ZwlrOutputManagerV1>,
|
||||
_manager_state: &OutputManagementManagerGlobalData,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let manager = data_init.init(manager, ());
|
||||
let g_state = state.output_management_state();
|
||||
let mut client_data = ClientData {
|
||||
heads: HashMap::new(),
|
||||
confs: HashMap::new(),
|
||||
manager: manager.clone(),
|
||||
};
|
||||
for (output, conf) in &g_state.current_state {
|
||||
send_new_head::<D>(display, client, &mut client_data, *output, conf);
|
||||
}
|
||||
g_state.clients.insert(client.id(), client_data);
|
||||
manager.done(g_state.serial);
|
||||
}
|
||||
|
||||
fn can_view(client: Client, global_data: &OutputManagementManagerGlobalData) -> bool {
|
||||
(global_data.filter)(&client)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrOutputManagerV1, (), D> for OutputManagementManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
state: &mut D,
|
||||
client: &Client,
|
||||
_manager: &ZwlrOutputManagerV1,
|
||||
request: zwlr_output_manager_v1::Request,
|
||||
_data: &(),
|
||||
_display: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
match request {
|
||||
zwlr_output_manager_v1::Request::CreateConfiguration { id, serial } => {
|
||||
let g_state = state.output_management_state();
|
||||
let conf = data_init.init(id, serial);
|
||||
if let Some(client_data) = g_state.clients.get_mut(&client.id()) {
|
||||
if serial != g_state.serial {
|
||||
conf.cancelled();
|
||||
}
|
||||
let state = OutputConfigurationState::Ongoing(HashMap::new());
|
||||
client_data.confs.insert(conf, state);
|
||||
} else {
|
||||
error!("CreateConfiguration: missing client data");
|
||||
}
|
||||
}
|
||||
zwlr_output_manager_v1::Request::Stop => {
|
||||
if let Some(c) = state.output_management_state().clients.remove(&client.id()) {
|
||||
c.manager.finished()
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
fn destroyed(state: &mut D, client: ClientId, _resource: &ZwlrOutputManagerV1, _data: &()) {
|
||||
state.output_management_state().clients.remove(&client);
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrOutputConfigurationV1, u32, D> for OutputManagementManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
state: &mut D,
|
||||
client: &Client,
|
||||
conf: &ZwlrOutputConfigurationV1,
|
||||
request: zwlr_output_configuration_v1::Request,
|
||||
serial: &u32,
|
||||
_display: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let g_state = state.output_management_state();
|
||||
let outdated = *serial != g_state.serial;
|
||||
if outdated {
|
||||
debug!("OutputConfiguration: request from an outdated configuration");
|
||||
}
|
||||
|
||||
let new_config = g_state
|
||||
.clients
|
||||
.get_mut(&client.id())
|
||||
.and_then(|data| data.confs.get_mut(conf));
|
||||
if new_config.is_none() {
|
||||
error!("OutputConfiguration: request from unknown configuration object");
|
||||
}
|
||||
|
||||
match request {
|
||||
zwlr_output_configuration_v1::Request::EnableHead { id, head } => {
|
||||
let Some(output) = head.data::<OutputId>() else {
|
||||
error!("EnableHead: Missing attached output");
|
||||
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
|
||||
return;
|
||||
};
|
||||
if outdated {
|
||||
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(new_config) = new_config else {
|
||||
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
|
||||
return;
|
||||
};
|
||||
|
||||
let OutputConfigurationState::Ongoing(new_config) = new_config else {
|
||||
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
|
||||
conf.post_error(
|
||||
zwlr_output_configuration_v1::Error::AlreadyUsed,
|
||||
"configuration had already been used",
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(current_config) = g_state.current_state.get(output) else {
|
||||
error!("EnableHead: output missing from current config");
|
||||
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
|
||||
return;
|
||||
};
|
||||
|
||||
match new_config.entry(*output) {
|
||||
Entry::Occupied(_) => {
|
||||
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
|
||||
conf.post_error(
|
||||
zwlr_output_configuration_v1::Error::AlreadyConfiguredHead,
|
||||
"head has been already configured",
|
||||
);
|
||||
return;
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let name = OutputName::from_ipc_output(current_config);
|
||||
let mut config = g_state
|
||||
.current_config
|
||||
.find(&name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| niri_config::Output {
|
||||
name: name.format_make_model_serial_or_connector(),
|
||||
..Default::default()
|
||||
});
|
||||
config.off = false;
|
||||
entry.insert(config);
|
||||
}
|
||||
};
|
||||
|
||||
data_init.init(id, OutputConfigurationHeadState::Ok(*output, conf.clone()));
|
||||
}
|
||||
zwlr_output_configuration_v1::Request::DisableHead { head } => {
|
||||
if outdated {
|
||||
return;
|
||||
}
|
||||
let Some(output) = head.data::<OutputId>() else {
|
||||
error!("DisableHead: missing attached output head name");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(new_config) = new_config else {
|
||||
return;
|
||||
};
|
||||
|
||||
let OutputConfigurationState::Ongoing(new_config) = new_config else {
|
||||
conf.post_error(
|
||||
zwlr_output_configuration_v1::Error::AlreadyUsed,
|
||||
"configuration had already been used",
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(current_config) = g_state.current_state.get(output) else {
|
||||
error!("EnableHead: output missing from current config");
|
||||
return;
|
||||
};
|
||||
|
||||
match new_config.entry(*output) {
|
||||
Entry::Occupied(_) => {
|
||||
conf.post_error(
|
||||
zwlr_output_configuration_v1::Error::AlreadyConfiguredHead,
|
||||
"head has been already configured",
|
||||
);
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let name = OutputName::from_ipc_output(current_config);
|
||||
let mut config = g_state
|
||||
.current_config
|
||||
.find(&name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| niri_config::Output {
|
||||
name: name.format_make_model_serial_or_connector(),
|
||||
..Default::default()
|
||||
});
|
||||
config.off = true;
|
||||
entry.insert(config);
|
||||
}
|
||||
};
|
||||
}
|
||||
zwlr_output_configuration_v1::Request::Apply => {
|
||||
if outdated {
|
||||
conf.cancelled();
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(new_config) = new_config else {
|
||||
return;
|
||||
};
|
||||
|
||||
let OutputConfigurationState::Ongoing(new_config) =
|
||||
mem::replace(new_config, OutputConfigurationState::Finished)
|
||||
else {
|
||||
conf.post_error(
|
||||
zwlr_output_configuration_v1::Error::AlreadyUsed,
|
||||
"configuration had already been used",
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let any_enabled = new_config.values().any(|c| !c.off);
|
||||
if !any_enabled {
|
||||
conf.failed();
|
||||
return;
|
||||
}
|
||||
|
||||
state.apply_output_config(new_config.into_values().collect());
|
||||
// FIXME: verify that it had been applied successfully (which may be difficult).
|
||||
conf.succeeded();
|
||||
}
|
||||
zwlr_output_configuration_v1::Request::Test => {
|
||||
if outdated {
|
||||
conf.cancelled();
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(new_config) = new_config else {
|
||||
return;
|
||||
};
|
||||
|
||||
let OutputConfigurationState::Ongoing(new_config) =
|
||||
mem::replace(new_config, OutputConfigurationState::Finished)
|
||||
else {
|
||||
conf.post_error(
|
||||
zwlr_output_configuration_v1::Error::AlreadyUsed,
|
||||
"configuration had already been used",
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let any_enabled = new_config.values().any(|c| !c.off);
|
||||
if !any_enabled {
|
||||
conf.failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: actually test the configuration with TTY.
|
||||
conf.succeeded()
|
||||
}
|
||||
zwlr_output_configuration_v1::Request::Destroy => {
|
||||
g_state
|
||||
.clients
|
||||
.get_mut(&client.id())
|
||||
.map(|d| d.confs.remove(conf));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState, D>
|
||||
for OutputManagementManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
state: &mut D,
|
||||
client: &Client,
|
||||
conf_head: &ZwlrOutputConfigurationHeadV1,
|
||||
request: zwlr_output_configuration_head_v1::Request,
|
||||
data: &OutputConfigurationHeadState,
|
||||
_display: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let g_state = state.output_management_state();
|
||||
let Some(client_data) = g_state.clients.get_mut(&client.id()) else {
|
||||
error!("ConfigurationHead: missing client data");
|
||||
return;
|
||||
};
|
||||
let OutputConfigurationHeadState::Ok(output_id, conf) = data else {
|
||||
warn!("ConfigurationHead: request sent to a cancelled head");
|
||||
return;
|
||||
};
|
||||
let Some(serial) = conf.data::<u32>() else {
|
||||
error!("ConfigurationHead: missing serial");
|
||||
return;
|
||||
};
|
||||
if *serial != g_state.serial {
|
||||
warn!("ConfigurationHead: request sent to an outdated");
|
||||
return;
|
||||
}
|
||||
let Some(new_config) = client_data.confs.get_mut(conf) else {
|
||||
error!("ConfigurationHead: unknown configuration");
|
||||
return;
|
||||
};
|
||||
let OutputConfigurationState::Ongoing(new_config) = new_config else {
|
||||
conf.post_error(
|
||||
zwlr_output_configuration_v1::Error::AlreadyUsed,
|
||||
"configuration had already been used",
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(new_config) = new_config.get_mut(output_id) else {
|
||||
error!("ConfigurationHead: config missing from enabled heads");
|
||||
return;
|
||||
};
|
||||
|
||||
match request {
|
||||
zwlr_output_configuration_head_v1::Request::SetMode { mode } => {
|
||||
let index = match client_data
|
||||
.heads
|
||||
.get(output_id)
|
||||
.map(|(_, mods)| mods.iter().position(|m| m.id() == mode.id()))
|
||||
{
|
||||
Some(Some(index)) => index,
|
||||
_ => {
|
||||
warn!("SetMode: failed to find requested mode");
|
||||
conf_head.post_error(
|
||||
zwlr_output_configuration_head_v1::Error::InvalidMode,
|
||||
"failed to find requested mode",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(current_config) = g_state.current_state.get(output_id) else {
|
||||
warn!("SetMode: output missing from the current config");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(mode) = current_config.modes.get(index) else {
|
||||
error!("SetMode: requested mode is out of range");
|
||||
return;
|
||||
};
|
||||
|
||||
new_config.mode = Some(niri_ipc::ConfiguredMode {
|
||||
width: mode.width,
|
||||
height: mode.height,
|
||||
refresh: Some(mode.refresh_rate as f64 / 1000.),
|
||||
});
|
||||
}
|
||||
zwlr_output_configuration_head_v1::Request::SetCustomMode {
|
||||
width,
|
||||
height,
|
||||
refresh,
|
||||
} => {
|
||||
// FIXME: Support custom mode
|
||||
let (width, height, refresh): (u16, u16, u32) =
|
||||
match (width.try_into(), height.try_into(), refresh.try_into()) {
|
||||
(Ok(width), Ok(height), Ok(refresh)) => (width, height, refresh),
|
||||
_ => {
|
||||
warn!("SetCustomMode: invalid input data");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(current_config) = g_state.current_state.get(output_id) else {
|
||||
warn!("SetMode: output missing from the current config");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(mode) = current_config.modes.iter().find(|m| {
|
||||
m.width == width
|
||||
&& m.height == height
|
||||
&& (refresh == 0 || m.refresh_rate == refresh)
|
||||
}) else {
|
||||
warn!("SetCustomMode: no matching mode");
|
||||
return;
|
||||
};
|
||||
|
||||
new_config.mode = Some(niri_ipc::ConfiguredMode {
|
||||
width: mode.width,
|
||||
height: mode.height,
|
||||
refresh: Some(mode.refresh_rate as f64 / 1000.),
|
||||
});
|
||||
}
|
||||
zwlr_output_configuration_head_v1::Request::SetPosition { x, y } => {
|
||||
new_config.position = Some(niri_config::Position { x, y });
|
||||
}
|
||||
zwlr_output_configuration_head_v1::Request::SetTransform { transform } => {
|
||||
let transform = match transform {
|
||||
WEnum::Value(WlTransform::Normal) => Transform::Normal,
|
||||
WEnum::Value(WlTransform::_90) => Transform::_90,
|
||||
WEnum::Value(WlTransform::_180) => Transform::_180,
|
||||
WEnum::Value(WlTransform::_270) => Transform::_270,
|
||||
WEnum::Value(WlTransform::Flipped) => Transform::Flipped,
|
||||
WEnum::Value(WlTransform::Flipped90) => Transform::Flipped90,
|
||||
WEnum::Value(WlTransform::Flipped180) => Transform::Flipped180,
|
||||
WEnum::Value(WlTransform::Flipped270) => Transform::Flipped270,
|
||||
_ => {
|
||||
warn!("SetTransform: unknown requested transform");
|
||||
conf_head.post_error(
|
||||
zwlr_output_configuration_head_v1::Error::InvalidTransform,
|
||||
"unknown transform value",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
new_config.transform = transform;
|
||||
}
|
||||
zwlr_output_configuration_head_v1::Request::SetScale { scale } => {
|
||||
if scale <= 0. {
|
||||
conf_head.post_error(
|
||||
zwlr_output_configuration_head_v1::Error::InvalidScale,
|
||||
"scale is negative or zero",
|
||||
);
|
||||
return;
|
||||
}
|
||||
new_config.scale = Some(FloatOrInt(scale));
|
||||
}
|
||||
zwlr_output_configuration_head_v1::Request::SetAdaptiveSync { state } => {
|
||||
let vrr = match state {
|
||||
WEnum::Value(AdaptiveSyncState::Enabled) => Some(Vrr { on_demand: false }),
|
||||
WEnum::Value(AdaptiveSyncState::Disabled) => None,
|
||||
_ => {
|
||||
warn!("SetAdaptativeSync: unknown requested adaptative sync");
|
||||
conf_head.post_error(
|
||||
zwlr_output_configuration_head_v1::Error::InvalidAdaptiveSyncState,
|
||||
"unknown adaptive sync value",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
new_config.variable_refresh_rate = vrr;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrOutputHeadV1, OutputId, D> for OutputManagementManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
_state: &mut D,
|
||||
_client: &Client,
|
||||
_output_head: &ZwlrOutputHeadV1,
|
||||
request: zwlr_output_head_v1::Request,
|
||||
_data: &OutputId,
|
||||
_display: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
match request {
|
||||
zwlr_output_head_v1::Request::Release => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
fn destroyed(state: &mut D, client: ClientId, _resource: &ZwlrOutputHeadV1, data: &OutputId) {
|
||||
if let Some(c) = state.output_management_state().clients.get_mut(&client) {
|
||||
c.heads.remove(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrOutputModeV1, (), D> for OutputManagementManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
_state: &mut D,
|
||||
_client: &Client,
|
||||
_mode: &ZwlrOutputModeV1,
|
||||
request: zwlr_output_mode_v1::Request,
|
||||
_data: &(),
|
||||
_display: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
match request {
|
||||
zwlr_output_mode_v1::Request::Release => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_output_management{
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_manager_v1::ZwlrOutputManagerV1: $crate::protocols::output_management::OutputManagementManagerGlobalData
|
||||
] => $crate::protocols::output_management::OutputManagementManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_manager_v1::ZwlrOutputManagerV1: ()
|
||||
] => $crate::protocols::output_management::OutputManagementManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_configuration_v1::ZwlrOutputConfigurationV1: u32
|
||||
] => $crate::protocols::output_management::OutputManagementManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_head_v1::ZwlrOutputHeadV1: $crate::backend::OutputId
|
||||
] => $crate::protocols::output_management::OutputManagementManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_mode_v1::ZwlrOutputModeV1: ()
|
||||
] => $crate::protocols::output_management::OutputManagementManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1: $crate::protocols::output_management::OutputConfigurationHeadState
|
||||
] => $crate::protocols::output_management::OutputManagementManagerState);
|
||||
};
|
||||
}
|
||||
|
||||
fn notify_removed_head(clients: &mut HashMap<ClientId, ClientData>, head: &OutputId) {
|
||||
for data in clients.values_mut() {
|
||||
if let Some((head, mods)) = data.heads.remove(head) {
|
||||
mods.iter().for_each(|m| m.finished());
|
||||
head.finished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_new_head(
|
||||
state: &mut OutputManagementManagerState,
|
||||
output: &OutputId,
|
||||
conf: &niri_ipc::Output,
|
||||
) {
|
||||
let display = &state.display;
|
||||
let clients = &mut state.clients;
|
||||
for data in clients.values_mut() {
|
||||
if let Some(client) = data.manager.client() {
|
||||
send_new_head::<State>(display, &client, data, *output, conf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_new_head<D>(
|
||||
display: &DisplayHandle,
|
||||
client: &Client,
|
||||
client_data: &mut ClientData,
|
||||
output: OutputId,
|
||||
conf: &niri_ipc::Output,
|
||||
) where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: 'static,
|
||||
{
|
||||
let new_head = client
|
||||
.create_resource::<ZwlrOutputHeadV1, _, D>(display, client_data.manager.version(), output)
|
||||
.unwrap();
|
||||
client_data.manager.head(&new_head);
|
||||
new_head.name(conf.name.clone());
|
||||
// Format matches what Output::new() does internally.
|
||||
new_head.description(format!("{} - {} - {}", conf.make, conf.model, conf.name));
|
||||
if let Some((width, height)) = conf.physical_size {
|
||||
if let (Ok(a), Ok(b)) = (width.try_into(), height.try_into()) {
|
||||
new_head.physical_size(a, b);
|
||||
}
|
||||
}
|
||||
let mut new_modes = Vec::with_capacity(conf.modes.len());
|
||||
for (index, mode) in conf.modes.iter().enumerate() {
|
||||
let new_mode = client
|
||||
.create_resource::<ZwlrOutputModeV1, _, D>(display, new_head.version(), ())
|
||||
.unwrap();
|
||||
new_head.mode(&new_mode);
|
||||
new_mode.size(i32::from(mode.width), i32::from(mode.height));
|
||||
if mode.is_preferred {
|
||||
new_mode.preferred();
|
||||
}
|
||||
if let Ok(refresh_rate) = mode.refresh_rate.try_into() {
|
||||
new_mode.refresh(refresh_rate);
|
||||
}
|
||||
if Some(index) == conf.current_mode {
|
||||
new_head.current_mode(&new_mode);
|
||||
}
|
||||
new_modes.push(new_mode);
|
||||
}
|
||||
if let Some(logical) = conf.logical {
|
||||
new_head.position(logical.x, logical.y);
|
||||
new_head.transform(ipc_transform_to_smithay(logical.transform).into());
|
||||
new_head.scale(logical.scale);
|
||||
}
|
||||
new_head.enabled(conf.current_mode.is_some() as i32);
|
||||
if new_head.version() >= zwlr_output_head_v1::EVT_MAKE_SINCE {
|
||||
new_head.make(conf.make.clone());
|
||||
}
|
||||
if new_head.version() >= zwlr_output_head_v1::EVT_MODEL_SINCE {
|
||||
new_head.model(conf.model.clone());
|
||||
}
|
||||
if new_head.version() >= zwlr_output_head_v1::EVT_SERIAL_NUMBER_SINCE {
|
||||
if let Some(serial) = &conf.serial {
|
||||
new_head.serial_number(serial.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if new_head.version() >= zwlr_output_head_v1::EVT_ADAPTIVE_SYNC_SINCE {
|
||||
new_head.adaptive_sync(match conf.vrr_enabled {
|
||||
true => AdaptiveSyncState::Enabled,
|
||||
false => AdaptiveSyncState::Disabled,
|
||||
});
|
||||
}
|
||||
// new_head.serial_number(output.serial);
|
||||
client_data.heads.insert(output, (new_head, new_modes));
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user