mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
Compare commits
676 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ba57fcf25 | |||
| 126ca37d96 | |||
| e6bd60fbb1 | |||
| a605a3f016 | |||
| ef44adea69 | |||
| 7fdb918cd0 | |||
| 8347cc20dc | |||
| 51a176ec4a | |||
| d618daf6b9 | |||
| 357f9157cc | |||
| c4a759e620 | |||
| f369a0f810 | |||
| 71251a7003 | |||
| 2415346caa | |||
| 3f2b7e63ba | |||
| ae89cb6017 | |||
| b6fc4d0455 | |||
| d8265ad34e | |||
| 3b864dc104 | |||
| 15093221ed | |||
| ac7b3fbf19 | |||
| bb8eb377c7 | |||
| 6169c0312a | |||
| 2ae99224ab | |||
| 4f63e13385 | |||
| 46a8f81160 | |||
| 0d6843ea67 | |||
| 6d083ea497 | |||
| 7a42140d6c | |||
| eeb411bef5 | |||
| defd4c5c4d | |||
| 7227e64149 | |||
| c98537a2b0 | |||
| 9c103f1f1d | |||
| 2aff1ec71a | |||
| 3466fc0a66 | |||
| f917932b3e | |||
| 89b7423ee5 | |||
| a2efaf2816 | |||
| 5816691460 | |||
| 4b5e9e6cb0 | |||
| a8259b4cea | |||
| 9d3d7cb0e9 | |||
| 398bc78ea0 | |||
| caa6189448 | |||
| 86f57c2ec7 | |||
| 3cc67897af | |||
| a99489c6c0 | |||
| 0763c7e196 | |||
| fb5c5204e8 | |||
| d207cd385b | |||
| 99bf2df2b4 | |||
| 09be90f4e6 | |||
| dfc42b9d82 | |||
| e2b9838d89 | |||
| 816a0d479c | |||
| 84323d10a4 | |||
| b956f2775c | |||
| 9ff2f83db0 | |||
| 7a10f71ee5 | |||
| ea7add3563 | |||
| e9c6f08906 | |||
| 17343a6740 | |||
| 140d726cd3 | |||
| c37d3b3442 | |||
| 497f186422 | |||
| 3e31c134a6 | |||
| fe682938db | |||
| 6142922ca4 | |||
| 4b44fba14c | |||
| 57639ca84c | |||
| ec88aae77d | |||
| 6c9705dd4b | |||
| eb590c5346 | |||
| 02baad91ac | |||
| 68589cd5a1 | |||
| f2c690802b | |||
| 9d6037b94c | |||
| 7b4cf094ef | |||
| 446bc155ce | |||
| 3289324ce4 | |||
| 9fb02b9571 | |||
| 0e9496b01e | |||
| 82dabc21f3 | |||
| 39b3d62873 | |||
| af080a03cd | |||
| 5f117c61dc | |||
| cb857e32e4 | |||
| 199be26947 | |||
| d5c0c74d2c | |||
| 9bb292ec82 | |||
| a1ba6bcaa0 | |||
| fd389af6d8 | |||
| db09727b18 | |||
| c9d6478c3c | |||
| 758cca5432 | |||
| 78e3daf5f8 | |||
| a99a0b2492 | |||
| bfd42c74f4 | |||
| 501ea47128 | |||
| d2a1cf53b4 | |||
| 62d47d77d5 | |||
| 85cd64e830 | |||
| 55c14eebf2 | |||
| 3fe67549b4 | |||
| 1835b532d9 | |||
| e6d82d3ee3 | |||
| fae3a27641 | |||
| 31e76cf451 | |||
| b8a9be542f | |||
| 59de6918b3 | |||
| bd3d554389 | |||
| af1fca35bb | |||
| 9571d149b2 | |||
| 99358e36b3 | |||
| 8b878f355f | |||
| 395b6d9a4f | |||
| 25f24f668c | |||
| 929eaf0d69 | |||
| ce3103949f | |||
| ef60dd81d7 | |||
| 7671a5d833 | |||
| 3f09352067 | |||
| 5059cce886 | |||
| b20dd226c0 | |||
| acb69c3b4d | |||
| dbe0a9e293 | |||
| d3a79faeec | |||
| 21630ddb5e | |||
| 9e5e0c85bb | |||
| 5cd8040d1a | |||
| 86351938f2 | |||
| ee4c5e23ab | |||
| ffd6acc0aa | |||
| cee11dc329 | |||
| 59a42249a4 | |||
| 74b016202b | |||
| 6ab055a4b9 | |||
| 98bd9b7abb | |||
| f36e1c2ef2 | |||
| 2243615fe9 | |||
| 7884d3bfea | |||
| fdbc485d78 | |||
| 7e253d2687 | |||
| 15ba2ab300 | |||
| 37840a418a | |||
| 4a4c972ffb | |||
| ba933773ab | |||
| f1cca1a6ca | |||
| 763cd564e3 | |||
| 95eafba346 | |||
| df94662435 | |||
| 430b155929 | |||
| c359d24825 | |||
| e8da89a430 | |||
| feae8c15e6 | |||
| b49f7dcb4d | |||
| 60034a57ef | |||
| 2adbf33fb6 | |||
| 28cc84fbd1 | |||
| e10b968eb0 | |||
| 3b1bf34e21 | |||
| bd927b54e0 | |||
| 66d3a3bd82 | |||
| 36489f1daa | |||
| b2c34e7fe9 | |||
| dcc291d701 | |||
| 8d43efe4ac | |||
| 3bb7e60311 | |||
| d639eb0032 | |||
| d91499486e | |||
| f7106f9658 | |||
| 835490c59a | |||
| 5cde00f6c6 | |||
| 7dc015e16b | |||
| 0db48e2f1b | |||
| 7cfecf4b1b | |||
| 3142838e9e | |||
| 4534d37266 | |||
| ec5112d779 | |||
| c709696237 | |||
| b271409509 | |||
| 500dcca9b7 | |||
| 7210045b2a | |||
| ed20822ce9 | |||
| e88dfae46f | |||
| f95d5a82df | |||
| 7f72c358d5 | |||
| 0d4f0f00c0 | |||
| f2663c738c | |||
| c3609efb7a | |||
| fd1f43673c | |||
| 9d10def7e8 | |||
| e251ca7340 | |||
| 9a527cc571 | |||
| 39f52b7585 | |||
| b447b1f4de | |||
| 1a0fab05b6 | |||
| fbb399f01d | |||
| 6a80ec4704 | |||
| e8b158641b | |||
| 27a715aded | |||
| 926e63a5f3 | |||
| e879199880 | |||
| 5b6b6a5fe1 | |||
| e11af089aa | |||
| 5e549e1323 | |||
| 287480b541 | |||
| a022fedd51 | |||
| a4b8e100c0 | |||
| 62576796be | |||
| 31891e6642 | |||
| 392fc27de1 | |||
| 9e560e7e60 | |||
| cee2ec7ab7 | |||
| 8c4ebb00a1 | |||
| f6aa8c1793 | |||
| a5d58d670b | |||
| b4922086ce | |||
| fd3b1f2b6c | |||
| ee0e2c7f1b | |||
| 4f16be9e4d | |||
| 0f30306fe5 | |||
| 1c6037e612 | |||
| fed86fdb5d | |||
| 3e21585861 | |||
| 9f9c4a99af | |||
| b220cdbe7e | |||
| df219b5134 | |||
| 8cdabe8adf | |||
| 8737067af5 | |||
| 50a99f6356 | |||
| 993c5ce8af | |||
| 47dd338340 | |||
| 87b6c12625 | |||
| b351f6ff22 | |||
| 12817a682d | |||
| 88614c08fe | |||
| 4f5c8e745b | |||
| f30413a744 | |||
| 3b8ce12316 | |||
| 880386e563 | |||
| 266c6c3878 | |||
| 7b033aa7c6 | |||
| efd8372b20 | |||
| 74a30be10b | |||
| 1c521e4831 | |||
| eda43b2b93 | |||
| 593241d2f0 | |||
| 69627bdc64 | |||
| 3fa373c720 | |||
| 083a56c729 | |||
| 88fcf0c2a9 | |||
| 26618f8d50 | |||
| 9f205d465c | |||
| d6e736aaf0 | |||
| 36b28d9b96 | |||
| 66113d7d76 | |||
| aa2e8b402c | |||
| 311f3be5d8 | |||
| 70dcd229cf | |||
| 26fe4a489a | |||
| 2363cf48e7 | |||
| 848294c09b | |||
| 693d935538 | |||
| 16405b9b2b | |||
| 4719cc6d59 | |||
| 98b92d4db7 | |||
| 1bdded7a44 | |||
| 9bfe90dee1 | |||
| c153349c62 | |||
| 5b6b5536fd | |||
| bac22dfe9f | |||
| bca6545288 | |||
| b94a5db879 | |||
| 4a4dcb85ef | |||
| 7b70cb66bc | |||
| cd6522bcc6 | |||
| 8885233c7e | |||
| 7478784343 | |||
| dca187de37 | |||
| fe660a253b | |||
| ad49e5820a | |||
| 4c40e6ce06 | |||
| 44c9797844 | |||
| 652d2923bb | |||
| 85349ce475 | |||
| 92cc2b89f7 | |||
| 078383ea82 | |||
| d27d6a504d | |||
| ec5144feca | |||
| 05e0e44a77 | |||
| 108e88e211 | |||
| a693f64c41 | |||
| 5c0468d469 | |||
| f2b1fc66f2 | |||
| 22302bf224 | |||
| bb6663ebac | |||
| c6e98d5a96 | |||
| d077350ae4 | |||
| f01c840ebe | |||
| ca1500ae90 | |||
| d7f3ca00c7 | |||
| fd8140e091 | |||
| d94fbe9895 | |||
| 7816f20e6a | |||
| 0d3610416c | |||
| 377ad54016 | |||
| 9e794f358b | |||
| 4e17cbb9ea | |||
| 4c98b87486 | |||
| 5b753be213 | |||
| a605e7f622 | |||
| 513488f6b8 | |||
| 43ea4a172a | |||
| d47b59879a | |||
| ef80bcc834 | |||
| eb8bd3894a | |||
| 7e552333a9 | |||
| 213eafa203 | |||
| 7b18ff8870 | |||
| 5246e2ff25 | |||
| dde9214ae4 | |||
| 29b7a41692 | |||
| 216753678a | |||
| b9e67f6565 | |||
| 3a481b5250 | |||
| 20769b4c2f | |||
| 14ac2cff4c | |||
| fde627d955 | |||
| 6942ecc13a | |||
| 963ff14ed0 | |||
| 96a3ded2ec | |||
| a21196ec54 | |||
| 0b83d9932b | |||
| 6bd92ab926 | |||
| 02eccf7762 | |||
| 89cf276779 | |||
| bc701cd529 | |||
| bfd81fc290 | |||
| 0dd8e883b0 | |||
| c31b58e2c9 | |||
| b163045757 | |||
| 41e9ec1364 | |||
| 64544a5726 | |||
| d7d5a7f8f6 | |||
| a451f75917 | |||
| 1515410012 | |||
| 8f9e0d029c | |||
| 90f24da631 | |||
| df70140b36 | |||
| f90eb0cbe4 | |||
| 55e2ea0c3b | |||
| 1d883931b4 | |||
| b65fad09d8 | |||
| 09a559d3c9 | |||
| 9fc749f3d4 | |||
| f836d1c28a | |||
| 4f05a74aa8 | |||
| c30f522ef2 | |||
| 397e704d64 | |||
| acc9d3e409 | |||
| 0c59fc304c | |||
| abd7f1dce3 | |||
| 1d87da00b7 | |||
| 91515ac6dc | |||
| 7ec771f7ec | |||
| a42a5ac696 | |||
| b31c0359eb | |||
| 934e5a6033 | |||
| 690d635505 | |||
| a444efd0eb | |||
| c41f93a468 | |||
| 900da597e4 | |||
| d320833f40 | |||
| c384b2489f | |||
| ddcac86d1d | |||
| 734e3a6d3c | |||
| f18b1a7043 | |||
| 7d24ad23c2 | |||
| 691bc064bb | |||
| 553b1ba852 | |||
| d5592743cb | |||
| 019e75955d | |||
| 32ad545f84 | |||
| 4eddcef1be | |||
| 68776f1cee | |||
| a0e2a15c60 | |||
| 88c6778771 | |||
| 73f6d3366e | |||
| 48a4d5c8a3 | |||
| 6f2f7fa259 | |||
| 49ddf66c2f | |||
| a169e0335d | |||
| e412a0fc6b | |||
| fb5fedbf24 | |||
| 6b04b1e454 | |||
| 0c340ec5ea | |||
| 34679c75a4 | |||
| 1d3820a064 | |||
| 1c749f578c | |||
| 3a887a6e49 | |||
| beef2da628 | |||
| 9b4d73f13a | |||
| 0226d9aec2 | |||
| 902222675a | |||
| ec43493522 | |||
| baa0518912 | |||
| d665079b84 | |||
| f0d935dee1 | |||
| 314b82caa0 | |||
| 8f79139b78 | |||
| c5296b870a | |||
| 78697d1cea | |||
| 852da5714a | |||
| 4f79303811 | |||
| f294d527e1 | |||
| 54a1cd5069 | |||
| 748d90b443 | |||
| 128b01e049 | |||
| 788c9c6c54 | |||
| a10705fb20 | |||
| b01b8afa8c | |||
| acd4cb51aa | |||
| 5ebcae997e | |||
| 2511a98e8b | |||
| a7692d10c4 | |||
| c892f04c96 | |||
| 3aad5a39ea | |||
| 7f025da5b6 | |||
| 01285bdbbe | |||
| 8182484572 | |||
| 0584dd2f1e | |||
| bd559a2660 | |||
| b4add625b2 | |||
| 890bbff007 | |||
| b853d5b124 | |||
| 693e0e09f7 | |||
| d52356b131 | |||
| b11b995d03 | |||
| 99ba295082 | |||
| 8c2b5957eb | |||
| 4472164447 | |||
| a3cbe3514b | |||
| efa7c862a4 | |||
| 0df7a085de | |||
| 6ae51f287c | |||
| 36076d5279 | |||
| 427c4e3982 | |||
| 1632ce87a5 | |||
| c523c80598 | |||
| 0bd6df507b | |||
| 6e41220dbf | |||
| e05bc269e6 | |||
| d574341f1f | |||
| 481958f8f7 | |||
| 4094469d59 | |||
| 2261fcb631 | |||
| 279c8b6aa2 | |||
| e35c630c1d | |||
| d3047afa7f | |||
| a03783f54c | |||
| cbf0d6190d | |||
| 89c991b636 | |||
| bbbd35e9ef | |||
| c308be315d | |||
| d825e3125e | |||
| 64288de04e | |||
| fb4471e69d | |||
| 8be8694f5f | |||
| 60b78dc2cd | |||
| 80fe5a8167 | |||
| df58c49876 | |||
| 7dee2f6995 | |||
| 623687e59b | |||
| 5958d3be62 | |||
| 142e57450d | |||
| 80815a1591 | |||
| 8412bfb813 | |||
| a0f279691a | |||
| 92aeddb9fe | |||
| d7da88853b | |||
| 89678c7b1e | |||
| 098c826095 | |||
| befbdc3ae5 | |||
| dca0364f4c | |||
| 37771259d9 | |||
| 4618e4851c | |||
| b2ca280c49 | |||
| bf6995f759 | |||
| ab0cce7cb7 | |||
| 2e422fc026 | |||
| a2f9d132a0 | |||
| 1973b97cc2 | |||
| b3c6f0e661 | |||
| 6998b17f9e | |||
| ed9932d70d | |||
| a5f3b2a949 | |||
| 152ed59502 | |||
| 8e16be9e11 | |||
| 300701f44e | |||
| d1370622d8 | |||
| 0134166009 | |||
| ddb9084260 | |||
| 0224452cef | |||
| c17d4dc050 | |||
| 4e33f45522 | |||
| b16d7abb35 | |||
| 2f17a30157 | |||
| 0dbd14ebdc | |||
| 8b3d8ccb47 | |||
| f8ff2e4e28 | |||
| 044f0d41a5 | |||
| 4089bebd83 | |||
| d4787c75fd | |||
| 3bf0a57b82 | |||
| cc505ae49f | |||
| 2f6de136dd | |||
| da21b50137 | |||
| a38a5c529f | |||
| 44b5612697 | |||
| 0113292cf6 | |||
| 4741ab2e04 | |||
| 08fb9435fd | |||
| 793e92e9d6 | |||
| a7c57f4faf | |||
| 8409107a5b | |||
| 9089c3fb02 | |||
| 6c897d5201 | |||
| 6cb5135f34 | |||
| 44bf45794e | |||
| d6da9f47d8 | |||
| be05b66ac3 | |||
| d1998ae3fa | |||
| 3c2e1554c6 | |||
| 744955ba69 | |||
| 7af33f9e6a | |||
| 3c0705b0ae | |||
| 4ea4d2bd3b | |||
| 6c52077d92 | |||
| 73bf7b1730 | |||
| b394cb6379 | |||
| 60854e180e | |||
| 5b4750a009 | |||
| ad50dd21fe | |||
| 8b0cb0bb57 | |||
| a24a6e4e3c | |||
| 6fba4c371e | |||
| 27911431db | |||
| db6447ed79 | |||
| 99c0fabee6 | |||
| fc99724aba | |||
| 88fbc62b1d | |||
| e8027d571f | |||
| daaee43be3 | |||
| 0d71cb93af | |||
| e5e50e82d5 | |||
| 7e852124a5 | |||
| f66a49bc42 | |||
| baf78ccda2 | |||
| 31f0e66f45 | |||
| 28b78a563b | |||
| 2f380de73b | |||
| e3a9a39c9a | |||
| 1710bb78df | |||
| 3e13fc3e70 | |||
| befc399506 | |||
| 88116b9fb1 | |||
| 53e1c58cc5 | |||
| 4b9ecdd11d | |||
| e31e409ee8 | |||
| 5488aaf69f | |||
| 96e493d8b1 | |||
| e409453fbd | |||
| 309bf1348c | |||
| 76a5635298 | |||
| f4f2a1f6de | |||
| a440805ea1 | |||
| c359672bd2 | |||
| 38350935e6 | |||
| 421cd89a0f | |||
| 5ce3369aa6 | |||
| f38acfe988 | |||
| 965619d096 | |||
| 9f017e834c | |||
| 3c67b08488 | |||
| 4add755a4d | |||
| 56e249aee6 | |||
| 6a7c8fcfd5 | |||
| 14b1003c62 | |||
| 43a4bae010 | |||
| 9c205f77a2 | |||
| c2e4cfd832 | |||
| c008e1c5bc | |||
| 1aa60f0da3 | |||
| bd1fd8383c | |||
| aac54d0ea1 | |||
| 4fe718581b | |||
| 71842f07bd | |||
| f2bec1f82f | |||
| 10460191b9 | |||
| c5fffd6e2c | |||
| 951f63b6fd | |||
| e6d8932b3b | |||
| 70f96cca0a | |||
| 4e357e9659 | |||
| 1f8aed6732 | |||
| fa2bace3cd | |||
| 955039b5ea | |||
| 771ea1e815 | |||
| d38bfc4aff | |||
| fbb0054232 | |||
| 2d3c36edae | |||
| 8dcc41a54d | |||
| ba3d2e36c8 | |||
| b51047ffcc | |||
| b1c40a9079 | |||
| b014c267ae | |||
| 6b16cc52db | |||
| d35ad73e35 | |||
| 2a1af3d9ae | |||
| 82e30246c1 | |||
| bb3a05bb3f | |||
| 40fa82275c | |||
| 9824321fc9 | |||
| 27e607ab82 | |||
| a2b27b8790 | |||
| 396089ef0e | |||
| df98b5021d | |||
| 34ce6d0b02 | |||
| 7af937b08e | |||
| 8665003269 | |||
| 1e76716819 | |||
| 91a42fdf58 | |||
| 5ed5243be6 | |||
| 4560251e64 | |||
| 2020dca3e0 | |||
| 7fc2121454 | |||
| 8b84afbd38 | |||
| 305fc3b557 | |||
| 61f2ac01d7 | |||
| 39a9f55205 | |||
| 11f351dbeb | |||
| 815fa379ea | |||
| 4c480a1ea3 | |||
| fa4aa0e06d | |||
| e2a6374bf5 | |||
| dc14554053 | |||
| 985ca7b643 | |||
| 60624d64fa | |||
| 2935dae89e | |||
| 4c22c3285d | |||
| 93cee2994a | |||
| 9c7e8d04d2 | |||
| 1e6b8906e0 | |||
| 6c5b92e5c0 | |||
| 38c515e12e | |||
| c239937fac | |||
| bafa574784 | |||
| 199a5854a8 | |||
| a74a578198 | |||
| 7de752ec56 | |||
| 0a833171ac | |||
| 1a0612cbfd | |||
| fbbd3ba349 | |||
| 1028639186 | |||
| 0e5e764c78 | |||
| db1faecc95 | |||
| c2c415d2e8 | |||
| d193928f31 | |||
| 17861e0003 | |||
| 97fe964e00 | |||
| 9debb5db23 | |||
| 494b438151 | |||
| 010a236882 | |||
| 1951d2a9f2 |
@@ -1 +1,12 @@
|
|||||||
|
# LFS configuration for images from the wiki
|
||||||
*.png filter=lfs diff=lfs merge=lfs -text
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# Exclude LFS-tracked files from the tarball
|
||||||
|
/wiki/img/ export-ignore
|
||||||
|
|
||||||
|
# exclude .gitattributes itself from the tarball
|
||||||
|
.gitattributes export-ignore
|
||||||
|
|
||||||
|
# tip: can be tested using
|
||||||
|
# git archive --format=tar.gz --output=source.tar.gz HEAD && \
|
||||||
|
# tar tfvz source.tar.gz | grep -e '.png' -e '.gitattributes'
|
||||||
|
|||||||
@@ -9,9 +9,23 @@ assignees: ''
|
|||||||
|
|
||||||
<!-- Please describe the issue here at the top, then fill in the system information below. -->
|
<!-- Please describe the issue here at the top, then fill in the system information below. -->
|
||||||
|
|
||||||
|
<!-- Attaching your full niri config can help diagnose the problem. -->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If you have a problem with a specific app, please verify that it is running on Wayland, rather than X11. An easy way is to run xeyes and mouse over the app: xeyes will be able to "see" only X11 windows.
|
||||||
|
|
||||||
|
You can also check what process the window PID belongs to:
|
||||||
|
|
||||||
|
$ readlink /proc/$(niri msg --json pick-window | jq .pid)/exe
|
||||||
|
|
||||||
|
If this points to xwayland-satellite, then it's an X11 window.
|
||||||
|
|
||||||
|
Please report issues with X11 apps to xwayland-satellite instead of niri: https://github.com/Supreeeme/xwayland-satellite/issues
|
||||||
|
-->
|
||||||
|
|
||||||
### System Information
|
### System Information
|
||||||
|
|
||||||
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
|
<!-- Paste the output of `niri -V`, e.g. niri 25.02 (b94a5db) -->
|
||||||
* niri version:
|
* niri version:
|
||||||
|
|
||||||
<!-- Write your distribution, e.g. Fedora 40 Silverblue -->
|
<!-- Write your distribution, e.g. Fedora 40 Silverblue -->
|
||||||
|
|||||||
@@ -2,3 +2,9 @@ contact_links:
|
|||||||
- name: Feature request
|
- name: Feature request
|
||||||
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
|
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
|
||||||
about: Ideas for new features and functionality (start a Discussion)
|
about: Ideas for new features and functionality (start a Discussion)
|
||||||
|
- name: Ask a question
|
||||||
|
url: https://github.com/YaLTeR/niri/discussions/new?category=q-a
|
||||||
|
about: Question about niri (start a Discussion)
|
||||||
|
- name: Matrix room
|
||||||
|
url: https://matrix.to/#/#niri:matrix.org
|
||||||
|
about: Chat about niri with other users
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "cargo"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
groups:
|
||||||
|
smithay:
|
||||||
|
patterns:
|
||||||
|
- "smithay"
|
||||||
|
- "smithay-drm-extras"
|
||||||
|
rust-dependencies:
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
ignore:
|
||||||
|
- dependency-name: "Andrew-Chen-Wang/github-wiki-action"
|
||||||
+75
-30
@@ -9,6 +9,8 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
RUN_SLOW_TESTS: 1
|
RUN_SLOW_TESTS: 1
|
||||||
|
DEPS_APT: curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
|
||||||
|
DEPS_DNF: cargo gcc clang libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel libdisplay-info-devel
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -23,8 +25,7 @@ jobs:
|
|||||||
release-flag: '--release'
|
release-flag: '--release'
|
||||||
|
|
||||||
name: test - ${{ matrix.configuration }}
|
name: test - ${{ matrix.configuration }}
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
container: ubuntu:23.10
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -33,8 +34,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
apt-get update -y
|
sudo 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 libdisplay-info-dev
|
sudo apt-get install -y ${{ env.DEPS_APT }}
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
@@ -63,19 +64,26 @@ jobs:
|
|||||||
- name: Build (with profiling)
|
- name: Build (with profiling)
|
||||||
run: cargo build ${{ matrix.release-flag }} --features profile-with-tracy
|
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 }}
|
run: cargo test --no-run --all --exclude niri-visual-tests ${{ matrix.release-flag }}
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
|
run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
|
||||||
|
|
||||||
visual-tests:
|
# Job that runs randomized tests for a longer period of time.
|
||||||
|
randomized-tests:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
name: visual tests
|
name: randomized tests
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
container: ubuntu:23.10
|
|
||||||
|
env:
|
||||||
|
RUST_BACKTRACE: 1
|
||||||
|
PROPTEST_CASES: 200000
|
||||||
|
PROPTEST_MAX_LOCAL_REJECTS: 200000
|
||||||
|
PROPTEST_MAX_GLOBAL_REJECTS: 200000
|
||||||
|
PROPTEST_MAX_SHRINK_ITERS: 200000
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -84,8 +92,35 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
apt-get update -y
|
sudo 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 libdisplay-info-dev
|
sudo apt-get install -y ${{ env.DEPS_APT }}
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Build tests
|
||||||
|
run: cargo test --no-run --all --exclude niri-visual-tests --release
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --all --exclude niri-visual-tests --release
|
||||||
|
|
||||||
|
visual-tests:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
name: visual tests
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
@@ -98,9 +133,8 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
name: 'msrv - 1.77.0'
|
name: msrv
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
container: ubuntu:23.10
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -109,10 +143,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
apt-get update -y
|
sudo 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 libdisplay-info-dev
|
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@1.77.0
|
- uses: dtolnay/rust-toolchain@1.80.1
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
@@ -123,8 +157,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
name: clippy
|
name: clippy
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
container: ubuntu:23.10
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -133,8 +166,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
apt-get update -y
|
sudo 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 libdisplay-info-dev
|
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
@@ -146,7 +179,7 @@ jobs:
|
|||||||
run: cargo clippy --all --all-targets
|
run: cargo clippy --all --all-targets
|
||||||
|
|
||||||
rustfmt:
|
rustfmt:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -161,8 +194,8 @@ jobs:
|
|||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
fedora:
|
fedora:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
container: fedora:39
|
container: fedora:41
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -172,13 +205,13 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo dnf update -y
|
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 libdisplay-info-devel
|
sudo dnf install -y ${{ env.DEPS_DNF }} libadwaita-devel
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: cargo build --all
|
- run: cargo build --all
|
||||||
|
|
||||||
nix:
|
nix:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -195,24 +228,36 @@ jobs:
|
|||||||
- run: nix flake check
|
- run: nix flake check
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
|
check-links:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: lycheeverse/lychee-action@v2.0.2 # later versions break fragment checks. don't bump until this is fixed: https://github.com/lycheeverse/lychee/issues/1574
|
||||||
|
with:
|
||||||
|
args: --offline --include-fragments 'wiki/*.md'
|
||||||
|
|
||||||
publish-wiki:
|
publish-wiki:
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
needs: build
|
needs:
|
||||||
|
- build
|
||||||
|
- check-links
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
show-progress: false
|
show-progress: false
|
||||||
- uses: Andrew-Chen-Wang/github-wiki-action@86138cbd6328b21d759e89ab6e6dd6a139b22270
|
- uses: Andrew-Chen-Wang/github-wiki-action@b7e552d7cb0fa7f83e459012ffc6840fd87bcb83
|
||||||
|
|
||||||
rustdoc:
|
rustdoc:
|
||||||
needs: build
|
needs: build
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -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
+854
-1055
File diff suppressed because it is too large
Load Diff
+53
-40
@@ -1,32 +1,38 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["niri-visual-tests"]
|
members = [
|
||||||
|
"niri-config",
|
||||||
|
"niri-ipc",
|
||||||
|
"niri-visual-tests",
|
||||||
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.10"
|
version = "25.5.1"
|
||||||
description = "A scrollable-tiling Wayland compositor"
|
description = "A scrollable-tiling Wayland compositor"
|
||||||
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
repository = "https://github.com/YaLTeR/niri"
|
repository = "https://github.com/YaLTeR/niri"
|
||||||
rust-version = "1.77"
|
rust-version = "1.80.1"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1.0.93"
|
anyhow = "1.0.98"
|
||||||
bitflags = "2.6.0"
|
bitflags = "2.9.1"
|
||||||
clap = { version = "4.5.20", features = ["derive"] }
|
clap = { version = "4.5.38", features = ["derive"] }
|
||||||
k9 = "0.12.0"
|
insta = "1.43.1"
|
||||||
serde = { version = "1.0.214", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.132"
|
serde_json = "1.0.140"
|
||||||
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
|
tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] }
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
tracy-client = { version = "0.17.4", default-features = false }
|
tracy-client = { version = "0.18.0", default-features = false }
|
||||||
|
|
||||||
[workspace.dependencies.smithay]
|
[workspace.dependencies.smithay]
|
||||||
|
# version = "0.4.1"
|
||||||
git = "https://github.com/Smithay/smithay.git"
|
git = "https://github.com/Smithay/smithay.git"
|
||||||
# path = "../smithay"
|
# path = "../smithay"
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
[workspace.dependencies.smithay-drm-extras]
|
[workspace.dependencies.smithay-drm-extras]
|
||||||
|
# version = "0.1.0"
|
||||||
git = "https://github.com/Smithay/smithay.git"
|
git = "https://github.com/Smithay/smithay.git"
|
||||||
# path = "../smithay/smithay-drm-extras"
|
# path = "../smithay/smithay-drm-extras"
|
||||||
|
|
||||||
@@ -47,45 +53,45 @@ keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
arrayvec = "0.7.6"
|
arrayvec = "0.7.6"
|
||||||
async-channel = "2.3.1"
|
async-channel = "2.3.1"
|
||||||
async-io = { version = "1.13.0", optional = true }
|
async-io = { version = "2.4.0", optional = true }
|
||||||
atomic = "0.6.0"
|
atomic = "0.6.0"
|
||||||
bitflags.workspace = true
|
bitflags.workspace = true
|
||||||
bytemuck = { version = "1.19.0", features = ["derive"] }
|
bytemuck = { version = "1.23.0", features = ["derive"] }
|
||||||
calloop = { version = "0.14.1", features = ["executor", "futures-io"] }
|
calloop = { version = "0.14.2", features = ["executor", "futures-io"] }
|
||||||
clap = { workspace = true, features = ["string"] }
|
clap = { workspace = true, features = ["string"] }
|
||||||
directories = "5.0.1"
|
clap_complete = "4.5.50"
|
||||||
|
directories = "6.0.0"
|
||||||
drm-ffi = "0.9.0"
|
drm-ffi = "0.9.0"
|
||||||
fastrand = "2.2.0"
|
fastrand = "2.3.0"
|
||||||
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
|
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
|
||||||
git-version = "0.3.9"
|
git-version = "0.3.9"
|
||||||
glam = "0.29.2"
|
glam = "0.30.3"
|
||||||
input = { version = "0.9.1", features = ["libinput_1_21"] }
|
input = { version = "0.9.1", features = ["libinput_1_21"] }
|
||||||
keyframe = { version = "1.1.1", default-features = false }
|
keyframe = { version = "1.1.1", default-features = false }
|
||||||
libc = "0.2.162"
|
libc = "0.2.172"
|
||||||
libdisplay-info = "0.1.0"
|
libdisplay-info = "0.2.2"
|
||||||
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
|
log = { version = "0.4.27", features = ["max_level_trace", "release_max_level_debug"] }
|
||||||
niri-config = { version = "0.1.10", path = "niri-config" }
|
niri-config = { version = "25.5.1", path = "niri-config" }
|
||||||
niri-ipc = { version = "0.1.10", path = "niri-ipc", features = ["clap"] }
|
niri-ipc = { version = "25.5.1", path = "niri-ipc", features = ["clap"] }
|
||||||
notify-rust = { version = "~4.10.0", optional = true }
|
ordered-float = "5.0.0"
|
||||||
ordered-float = "4.5.0"
|
pango = { version = "0.20.10", features = ["v1_44"] }
|
||||||
pango = { version = "0.20.4", features = ["v1_44"] }
|
pangocairo = "0.20.10"
|
||||||
pangocairo = "0.20.4"
|
|
||||||
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
|
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
|
||||||
png = "0.17.14"
|
png = "0.17.16"
|
||||||
portable-atomic = { version = "1.9.0", default-features = false, features = ["float"] }
|
portable-atomic = { version = "1.11.0", default-features = false, features = ["float"] }
|
||||||
profiling = "1.0.16"
|
profiling = "1.0.16"
|
||||||
sd-notify = "0.4.3"
|
sd-notify = "0.4.5"
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
smithay-drm-extras.workspace = true
|
smithay-drm-extras.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracy-client.workspace = true
|
tracy-client.workspace = true
|
||||||
url = { version = "2.5.3", optional = true }
|
url = { version = "2.5.4", optional = true }
|
||||||
wayland-backend = "0.3.7"
|
wayland-backend = "0.3.10"
|
||||||
wayland-scanner = "0.31.5"
|
wayland-scanner = "0.31.6"
|
||||||
xcursor = "0.3.8"
|
xcursor = "0.3.8"
|
||||||
zbus = { version = "~3.15.2", optional = true }
|
zbus = { version = "5.7.0", optional = true }
|
||||||
|
|
||||||
[dependencies.smithay]
|
[dependencies.smithay]
|
||||||
workspace = true
|
workspace = true
|
||||||
@@ -107,15 +113,18 @@ features = [
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
approx = "0.5.1"
|
approx = "0.5.1"
|
||||||
k9.workspace = true
|
calloop-wayland-source = "0.4.0"
|
||||||
proptest = "1.5.0"
|
insta.workspace = true
|
||||||
proptest-derive = { version = "0.5.0", features = ["boxed_union"] }
|
proptest = "1.6.0"
|
||||||
xshell = "0.2.6"
|
proptest-derive = { version = "0.5.1", features = ["boxed_union"] }
|
||||||
|
rayon = "1.10.0"
|
||||||
|
wayland-client = "0.31.10"
|
||||||
|
xshell = "0.2.7"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["dbus", "systemd", "xdp-gnome-screencast"]
|
default = ["dbus", "systemd", "xdp-gnome-screencast"]
|
||||||
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
|
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
|
||||||
dbus = ["zbus", "async-io", "notify-rust", "url"]
|
dbus = ["dep:zbus", "dep:async-io", "dep:url"]
|
||||||
# Enables systemd integration (global environment, apps in transient scopes).
|
# Enables systemd integration (global environment, apps in transient scopes).
|
||||||
systemd = ["dbus"]
|
systemd = ["dbus"]
|
||||||
# Enables screencasting support through xdg-desktop-portal-gnome.
|
# Enables screencasting support through xdg-desktop-portal-gnome.
|
||||||
@@ -138,8 +147,12 @@ lto = "thin"
|
|||||||
# knuffel with chomsky generates a metric ton of debuginfo.
|
# knuffel with chomsky generates a metric ton of debuginfo.
|
||||||
debug = false
|
debug = false
|
||||||
|
|
||||||
|
[profile.dev.package]
|
||||||
|
insta.opt-level = 3
|
||||||
|
similar.opt-level = 3
|
||||||
|
|
||||||
[package.metadata.generate-rpm]
|
[package.metadata.generate-rpm]
|
||||||
version = "0.1.10"
|
version = "25.02"
|
||||||
assets = [
|
assets = [
|
||||||
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
||||||
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<h1 align="center">niri</h1>
|
<h1 align="center">niri</h1>
|
||||||
<p align="center">A scrollable-tiling Wayland compositor.</p>
|
<p align="center">A scrollable-tiling Wayland compositor.</p>
|
||||||
<p align="center">
|
<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/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>
|
<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>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<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>
|
<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>
|
</p>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
@@ -28,12 +28,15 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Scrollable tiling
|
- Built from the ground up for scrollable tiling
|
||||||
- Dynamic workspaces like in GNOME
|
- [Dynamic workspaces](https://github.com/YaLTeR/niri/wiki/Workspaces) like in GNOME
|
||||||
|
- An [Overview](https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995) that zooms out workspaces and windows
|
||||||
- Built-in screenshot UI
|
- Built-in screenshot UI
|
||||||
- Monitor and window 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
|
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
|
||||||
|
- [Dynamic cast target](https://github.com/YaLTeR/niri/wiki/Screencasting#dynamic-screencast-target) that can change what it shows on the go
|
||||||
- [Touchpad](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/YaLTeR/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
|
- [Touchpad](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/YaLTeR/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
|
||||||
|
- Group windows into [tabs](https://github.com/YaLTeR/niri/wiki/Tabs)
|
||||||
- Configurable layout: gaps, borders, struts, window sizes
|
- Configurable layout: gaps, borders, struts, window sizes
|
||||||
- [Gradient borders](https://github.com/YaLTeR/niri/wiki/Configuration:-Layout#gradients) with Oklab and Oklch support
|
- [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)
|
- [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)
|
||||||
@@ -45,10 +48,35 @@ https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f97
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
A lot of the essential functionality is implemented, plus some goodies on top.
|
Niri is stable for day-to-day use and does most things expected of a Wayland compositor.
|
||||||
Feel free to give niri a try: follow the instructions on the [Getting Started](https://github.com/YaLTeR/niri/wiki/Getting-Started) wiki page.
|
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.
|
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
|
||||||
|
|
||||||
|
Here are some points you may have questions about:
|
||||||
|
|
||||||
|
- **Multi-monitor**: yes, a core part of the design from the very start. Mixed DPI works.
|
||||||
|
- **Fractional scaling**: yes, plus all niri UI stays pixel-perfect.
|
||||||
|
- **NVIDIA**: seems to work fine.
|
||||||
|
- **Floating windows**: yes, starting from niri 25.01.
|
||||||
|
- **Input devices**: niri supports tablets, touchpads, and touchscreens.
|
||||||
|
You can map the tablet to a specific monitor, or use [OpenTabletDriver].
|
||||||
|
We have touchpad gestures, but no touchscreen gestures yet.
|
||||||
|
- **Wlr protocols**: yes, we have most of the important ones like layer-shell, gamma-control, screencopy.
|
||||||
|
You can check on [wayland.app](https://wayland.app) at the bottom of each protocol's page.
|
||||||
|
- **Performance**: while I run niri on beefy machines, I try to stay conscious of performance.
|
||||||
|
I've seen someone use it fine on an Eee PC 900 from 2008, of all things.
|
||||||
|
- **Xwayland**: no built-in support, but xwayland-satellite is [easy to set up](https://github.com/YaLTeR/niri/wiki/Xwayland#using-xwayland-satellite) and works very well.
|
||||||
|
- Steam and games, including Proton: work perfectly through xwayland-satellite.
|
||||||
|
- JetBrains IDEs, Ghidra: work well through xwayland-satellite.
|
||||||
|
- Discord and other Electron apps: work well through xwayland-satellite.
|
||||||
|
- Chromium and VSCode: work perfectly natively on Wayland with the right flags.
|
||||||
|
- X11 apps that want to position windows or bars at specific screen coordinates: won't work well; you can run them in a nested compositor like [labwc](https://github.com/YaLTeR/niri/wiki/Xwayland#using-the-labwc-wayland-compositor) or [rootful Xwayland](https://github.com/YaLTeR/niri/wiki/Xwayland#directly-running-xwayland-in-rootful-mode).
|
||||||
|
- Display scaling (integer or fractional) keeps X11 apps crisp, but you need the latest xwayland-satellite.
|
||||||
|
For games, you can run them in [gamescope] at native resolution, even with display scaling.
|
||||||
|
|
||||||
## Inspiration
|
## Inspiration
|
||||||
|
|
||||||
Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top of GNOME Shell.
|
Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top of GNOME Shell.
|
||||||
@@ -62,10 +90,17 @@ Here are some other projects which implement a similar workflow:
|
|||||||
|
|
||||||
- [PaperWM]: scrollable tiling on top of GNOME Shell.
|
- [PaperWM]: scrollable tiling on top of GNOME Shell.
|
||||||
- [karousel]: scrollable tiling on top of KDE.
|
- [karousel]: scrollable tiling on top of KDE.
|
||||||
- [papersway]: scrollable tiling on top of sway/i3.
|
- [scroll](https://github.com/dawsers/scroll) and [papersway]: scrollable tiling on top of sway/i3.
|
||||||
- [hyprscroller] and [hyprslidr]: scrollable tiling on top of Hyprland.
|
- [hyprscrolling] and [hyprslidr]: scrollable tiling on top of Hyprland.
|
||||||
- [PaperWM.spoon]: scrollable tiling on top of macOS.
|
- [PaperWM.spoon]: scrollable tiling on top of macOS.
|
||||||
|
|
||||||
|
## Media
|
||||||
|
|
||||||
|
[niri: Making a Wayland compositor in Rust](https://youtu.be/Kmz8ODolnDg?list=PLRdS-n5seLRqrmWDQY4KDqtRMfIwU0U3T)
|
||||||
|
|
||||||
|
My talk from the 2024 Moscow RustCon about niri, and how I do randomized property testing and profiling, and measure input latency.
|
||||||
|
The talk is in Russian, but I prepared full English subtitles that you can find in YouTube's subtitle language selector.
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
|
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
|
||||||
@@ -75,6 +110,9 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
|
|||||||
[fuzzel]: https://codeberg.org/dnkl/fuzzel
|
[fuzzel]: https://codeberg.org/dnkl/fuzzel
|
||||||
[karousel]: https://github.com/peterfajdiga/karousel
|
[karousel]: https://github.com/peterfajdiga/karousel
|
||||||
[papersway]: https://spwhitton.name/tech/code/papersway/
|
[papersway]: https://spwhitton.name/tech/code/papersway/
|
||||||
[hyprscroller]: https://github.com/dawsers/hyprscroller
|
[hyprscrolling]: https://github.com/hyprwm/hyprland-plugins/tree/main/hyprscrolling
|
||||||
[hyprslidr]: https://gitlab.com/magus/hyprslidr
|
[hyprslidr]: https://gitlab.com/magus/hyprslidr
|
||||||
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
|
[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
|
||||||
|
|||||||
Generated
+9
-9
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nix-filter": {
|
"nix-filter": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1710156097,
|
"lastModified": 1731533336,
|
||||||
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
|
"narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "nix-filter",
|
"repo": "nix-filter",
|
||||||
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
|
"rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -17,11 +17,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1726365531,
|
"lastModified": 1742707865,
|
||||||
"narHash": "sha256-luAKNxWZ+ZN0kaHchx1OdLQ71n81Y31ryNPWP1YRDZc=",
|
"narHash": "sha256-RVQQZy38O3Zb8yoRJhuFgWo/iDIDj0hEdRTVfhOtzRk=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9299cdf978e15f448cf82667b0ffdd480b44ee48",
|
"rev": "dd613136ee91f67e5dba3f3f41ac99ae89c5406b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -45,11 +45,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1727663505,
|
"lastModified": 1742697269,
|
||||||
"narHash": "sha256-83j/GrHsx8GFUcQofKh+PRPz6pz8sxAsZyT/HCNdey8=",
|
"narHash": "sha256-Lpp0XyAtIl1oGJzNmTiTGLhTkcUjwSkEb0gOiNzYFGM=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "c2099c6c7599ea1980151b8b6247a8f93e1806ee",
|
"rev": "01973c84732f9275c50c5f075dd1f54cc04b3316",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -26,21 +26,20 @@
|
|||||||
{
|
{
|
||||||
lib,
|
lib,
|
||||||
cairo,
|
cairo,
|
||||||
clang,
|
|
||||||
dbus,
|
dbus,
|
||||||
libGL,
|
libGL,
|
||||||
libclang,
|
|
||||||
libdisplay-info,
|
libdisplay-info,
|
||||||
libinput,
|
libinput,
|
||||||
seatd,
|
seatd,
|
||||||
libxkbcommon,
|
libxkbcommon,
|
||||||
mesa,
|
libgbm,
|
||||||
pango,
|
pango,
|
||||||
pipewire,
|
pipewire,
|
||||||
pkg-config,
|
pkg-config,
|
||||||
rustPlatform,
|
rustPlatform,
|
||||||
systemd,
|
systemd,
|
||||||
wayland,
|
wayland,
|
||||||
|
installShellFiles,
|
||||||
withDbus ? true,
|
withDbus ? true,
|
||||||
withSystemd ? true,
|
withSystemd ? true,
|
||||||
withScreencastSupport ? true,
|
withScreencastSupport ? true,
|
||||||
@@ -79,8 +78,9 @@
|
|||||||
strictDeps = true;
|
strictDeps = true;
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
clang
|
rustPlatform.bindgenHook
|
||||||
pkg-config
|
pkg-config
|
||||||
|
installShellFiles
|
||||||
];
|
];
|
||||||
|
|
||||||
buildInputs =
|
buildInputs =
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
libinput
|
libinput
|
||||||
seatd
|
seatd
|
||||||
libxkbcommon
|
libxkbcommon
|
||||||
mesa # libgbm
|
libgbm
|
||||||
pango
|
pango
|
||||||
wayland
|
wayland
|
||||||
]
|
]
|
||||||
@@ -108,8 +108,22 @@
|
|||||||
++ lib.optional withSystemd "systemd";
|
++ lib.optional withSystemd "systemd";
|
||||||
buildNoDefaultFeatures = true;
|
buildNoDefaultFeatures = true;
|
||||||
|
|
||||||
|
# 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 =
|
postInstall =
|
||||||
''
|
''
|
||||||
|
installShellCompletion --cmd niri \
|
||||||
|
--bash <($out/bin/niri completions bash) \
|
||||||
|
--fish <($out/bin/niri completions fish) \
|
||||||
|
--zsh <($out/bin/niri completions zsh)
|
||||||
|
|
||||||
install -Dm644 resources/niri.desktop -t $out/share/wayland-sessions
|
install -Dm644 resources/niri.desktop -t $out/share/wayland-sessions
|
||||||
install -Dm644 resources/niri-portals.conf -t $out/share/xdg-desktop-portal
|
install -Dm644 resources/niri-portals.conf -t $out/share/xdg-desktop-portal
|
||||||
''
|
''
|
||||||
@@ -119,8 +133,6 @@
|
|||||||
'';
|
'';
|
||||||
|
|
||||||
env = {
|
env = {
|
||||||
LIBCLANG_PATH = lib.getLib libclang + "/lib";
|
|
||||||
|
|
||||||
# Force linking with libEGL and libwayland-client
|
# Force linking with libEGL and libwayland-client
|
||||||
# so they can be discovered by `dlopen()`
|
# so they can be discovered by `dlopen()`
|
||||||
RUSTFLAGS = toString (
|
RUSTFLAGS = toString (
|
||||||
@@ -191,7 +203,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
pkgs.clang
|
pkgs.rustPlatform.bindgenHook
|
||||||
pkgs.pkg-config
|
pkgs.pkg-config
|
||||||
pkgs.wrapGAppsHook4 # For `niri-visual-tests`
|
pkgs.wrapGAppsHook4 # For `niri-visual-tests`
|
||||||
];
|
];
|
||||||
@@ -201,8 +213,6 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
env = {
|
env = {
|
||||||
inherit (niri) LIBCLANG_PATH;
|
|
||||||
|
|
||||||
# WARN: Do not overwrite this variable in your shell!
|
# WARN: Do not overwrite this variable in your shell!
|
||||||
# It is required for `dlopen()` to work on some libraries; see the comment
|
# It is required for `dlopen()` to work on some libraries; see the comment
|
||||||
# in the package expression
|
# in the package expression
|
||||||
|
|||||||
@@ -11,14 +11,13 @@ repository.workspace = true
|
|||||||
bitflags.workspace = true
|
bitflags.workspace = true
|
||||||
csscolorparser = "0.7.0"
|
csscolorparser = "0.7.0"
|
||||||
knuffel = "3.2.0"
|
knuffel = "3.2.0"
|
||||||
miette = "5.10.0"
|
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
|
||||||
niri-ipc = { version = "0.1.10", path = "../niri-ipc" }
|
niri-ipc = { version = "25.5.1", path = "../niri-ipc" }
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
smithay = { workspace = true, features = ["backend_libinput"] }
|
smithay = { workspace = true, features = ["backend_libinput"] }
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracy-client.workspace = true
|
tracy-client.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
k9.workspace = true
|
insta.workspace = true
|
||||||
miette = { version = "5.10.0", features = ["fancy"] }
|
|
||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
use crate::{BlockOutFrom, CornerRadius, RegexEq, ShadowRule};
|
||||||
|
|
||||||
|
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||||
|
pub struct LayerRule {
|
||||||
|
#[knuffel(children(name = "match"))]
|
||||||
|
pub matches: Vec<Match>,
|
||||||
|
#[knuffel(children(name = "exclude"))]
|
||||||
|
pub excludes: Vec<Match>,
|
||||||
|
|
||||||
|
#[knuffel(child, unwrap(argument))]
|
||||||
|
pub opacity: Option<f32>,
|
||||||
|
#[knuffel(child, unwrap(argument))]
|
||||||
|
pub block_out_from: Option<BlockOutFrom>,
|
||||||
|
#[knuffel(child, default)]
|
||||||
|
pub shadow: ShadowRule,
|
||||||
|
#[knuffel(child)]
|
||||||
|
pub geometry_corner_radius: Option<CornerRadius>,
|
||||||
|
#[knuffel(child, unwrap(argument))]
|
||||||
|
pub place_within_backdrop: Option<bool>,
|
||||||
|
#[knuffel(child, unwrap(argument))]
|
||||||
|
pub baba_is_float: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||||
|
pub struct Match {
|
||||||
|
#[knuffel(property, str)]
|
||||||
|
pub namespace: Option<RegexEq>,
|
||||||
|
#[knuffel(property)]
|
||||||
|
pub at_startup: Option<bool>,
|
||||||
|
}
|
||||||
+2211
-352
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -13,7 +13,7 @@ readme = "README.md"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { workspace = true, optional = true }
|
clap = { workspace = true, optional = true }
|
||||||
schemars = { version = "0.8.21", optional = true }
|
schemars = { version = "0.8.22", optional = true }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -12,5 +12,5 @@ Use an exact version requirement to avoid breaking changes:
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
niri-ipc = "=0.1.10"
|
niri-ipc = "=25.5.1"
|
||||||
```
|
```
|
||||||
|
|||||||
+556
-12
@@ -1,8 +1,23 @@
|
|||||||
//! Types for communicating with niri via IPC.
|
//! Types for communicating with niri via IPC.
|
||||||
//!
|
//!
|
||||||
//! After connecting to the niri socket, you can send a single [`Request`] and receive a single
|
//! After connecting to the niri socket, you can send [`Request`]s. Niri will process them one by
|
||||||
//! [`Reply`], which is a `Result` wrapping a [`Response`]. If you requested an event stream, you
|
//! one, in order, and to each request it will respond with a single [`Reply`], which is a `Result`
|
||||||
//! can keep reading [`Event`]s from the socket after the response.
|
//! wrapping a [`Response`].
|
||||||
|
//!
|
||||||
|
//! If you send a [`Request::EventStream`], niri will *stop* reading subsequent [`Request`]s, and
|
||||||
|
//! will start continuously writing compositor [`Event`]s to the socket. If you'd like to read an
|
||||||
|
//! event stream and write more requests at the same time, you need to use two IPC sockets.
|
||||||
|
//!
|
||||||
|
//! <div class="warning">
|
||||||
|
//!
|
||||||
|
//! Requests are *always* processed separately. Time passes between requests, even when sending
|
||||||
|
//! multiple requests to the socket at once. For example, sending [`Request::Workspaces`] and
|
||||||
|
//! [`Request::Windows`] together may not return consistent results (e.g. a window may open on a
|
||||||
|
//! new workspace in-between the two responses). This goes for actions too: sending
|
||||||
|
//! [`Action::FocusWindow`] and <code>[Action::CloseWindow] { id: None }</code> together may close
|
||||||
|
//! the wrong window because a different window got focused in-between these requests.
|
||||||
|
//!
|
||||||
|
//! </div>
|
||||||
//!
|
//!
|
||||||
//! You can use the [`socket::Socket`] helper if you're fine with blocking communication. However,
|
//! You can use the [`socket::Socket`] helper if you're fine with blocking communication. However,
|
||||||
//! it is a fairly simple helper, so if you need async, or if you're using a different language,
|
//! it is a fairly simple helper, so if you need async, or if you're using a different language,
|
||||||
@@ -12,7 +27,9 @@
|
|||||||
//! 2. Connect to the socket and write a JSON-formatted [`Request`] on a single line. You can follow
|
//! 2. Connect to the socket and write a JSON-formatted [`Request`] on a single line. You can follow
|
||||||
//! up with a line break and a flush, or just flush and shutdown the write end of the socket.
|
//! up with a line break and a flush, or just flush and shutdown the write end of the socket.
|
||||||
//! 3. Niri will respond with a single line JSON-formatted [`Reply`].
|
//! 3. Niri will respond with a single line JSON-formatted [`Reply`].
|
||||||
//! 4. If you requested an event stream, niri will keep responding with JSON-formatted [`Event`]s,
|
//! 4. You can keep writing [`Request`]s, each on a single line, and read [`Reply`]s, also each on a
|
||||||
|
//! separate line.
|
||||||
|
//! 5. After you request an event stream, niri will keep responding with JSON-formatted [`Event`]s,
|
||||||
//! on a single line each.
|
//! on a single line each.
|
||||||
//!
|
//!
|
||||||
//! ## Backwards compatibility
|
//! ## Backwards compatibility
|
||||||
@@ -24,7 +41,7 @@
|
|||||||
//!
|
//!
|
||||||
//! ```toml
|
//! ```toml
|
||||||
//! [dependencies]
|
//! [dependencies]
|
||||||
//! niri-ipc = "=0.1.10"
|
//! niri-ipc = "=25.5.1"
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ## Features
|
//! ## Features
|
||||||
@@ -55,12 +72,18 @@ pub enum Request {
|
|||||||
Workspaces,
|
Workspaces,
|
||||||
/// Request information about open windows.
|
/// Request information about open windows.
|
||||||
Windows,
|
Windows,
|
||||||
|
/// Request information about layer-shell surfaces.
|
||||||
|
Layers,
|
||||||
/// Request information about the configured keyboard layouts.
|
/// Request information about the configured keyboard layouts.
|
||||||
KeyboardLayouts,
|
KeyboardLayouts,
|
||||||
/// Request information about the focused output.
|
/// Request information about the focused output.
|
||||||
FocusedOutput,
|
FocusedOutput,
|
||||||
/// Request information about the focused window.
|
/// Request information about the focused window.
|
||||||
FocusedWindow,
|
FocusedWindow,
|
||||||
|
/// Request picking a window and get its information.
|
||||||
|
PickWindow,
|
||||||
|
/// Request picking a color from the screen.
|
||||||
|
PickColor,
|
||||||
/// Perform an action.
|
/// Perform an action.
|
||||||
Action(Action),
|
Action(Action),
|
||||||
/// Change output configuration temporarily.
|
/// Change output configuration temporarily.
|
||||||
@@ -91,6 +114,8 @@ pub enum Request {
|
|||||||
EventStream,
|
EventStream,
|
||||||
/// Respond with an error (for testing error handling).
|
/// Respond with an error (for testing error handling).
|
||||||
ReturnError,
|
ReturnError,
|
||||||
|
/// Request information about the overview.
|
||||||
|
OverviewState,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reply from niri to client.
|
/// Reply from niri to client.
|
||||||
@@ -119,14 +144,38 @@ pub enum Response {
|
|||||||
Workspaces(Vec<Workspace>),
|
Workspaces(Vec<Workspace>),
|
||||||
/// Information about open windows.
|
/// Information about open windows.
|
||||||
Windows(Vec<Window>),
|
Windows(Vec<Window>),
|
||||||
|
/// Information about layer-shell surfaces.
|
||||||
|
Layers(Vec<LayerSurface>),
|
||||||
/// Information about the keyboard layout.
|
/// Information about the keyboard layout.
|
||||||
KeyboardLayouts(KeyboardLayouts),
|
KeyboardLayouts(KeyboardLayouts),
|
||||||
/// Information about the focused output.
|
/// Information about the focused output.
|
||||||
FocusedOutput(Option<Output>),
|
FocusedOutput(Option<Output>),
|
||||||
/// Information about the focused window.
|
/// Information about the focused window.
|
||||||
FocusedWindow(Option<Window>),
|
FocusedWindow(Option<Window>),
|
||||||
|
/// Information about the picked window.
|
||||||
|
PickedWindow(Option<Window>),
|
||||||
|
/// Information about the picked color.
|
||||||
|
PickedColor(Option<PickedColor>),
|
||||||
/// Output configuration change result.
|
/// Output configuration change result.
|
||||||
OutputConfigChanged(OutputConfigChanged),
|
OutputConfigChanged(OutputConfigChanged),
|
||||||
|
/// Information about the overview.
|
||||||
|
OverviewState(Overview),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overview information.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
pub struct Overview {
|
||||||
|
/// Whether the overview is currently open.
|
||||||
|
pub is_open: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Color picked from the screen.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
pub struct PickedColor {
|
||||||
|
/// Color values as red, green, blue, each ranging from 0.0 to 1.0.
|
||||||
|
pub rgb: [f64; 3],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actions that niri can perform.
|
/// Actions that niri can perform.
|
||||||
@@ -161,9 +210,23 @@ pub enum Action {
|
|||||||
delay_ms: Option<u16>,
|
delay_ms: Option<u16>,
|
||||||
},
|
},
|
||||||
/// Open the screenshot UI.
|
/// Open the screenshot UI.
|
||||||
Screenshot {},
|
Screenshot {
|
||||||
|
/// Whether to show the mouse pointer by default in the screenshot UI.
|
||||||
|
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||||
|
show_pointer: bool,
|
||||||
|
},
|
||||||
/// Screenshot the focused screen.
|
/// Screenshot the focused screen.
|
||||||
ScreenshotScreen {},
|
ScreenshotScreen {
|
||||||
|
/// Write the screenshot to disk in addition to putting it in your clipboard.
|
||||||
|
///
|
||||||
|
/// The screenshot is saved according to the `screenshot-path` config setting.
|
||||||
|
#[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||||
|
write_to_disk: bool,
|
||||||
|
|
||||||
|
/// Whether to include the mouse pointer in the screenshot.
|
||||||
|
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||||
|
show_pointer: bool,
|
||||||
|
},
|
||||||
/// Screenshot a window.
|
/// Screenshot a window.
|
||||||
#[cfg_attr(feature = "clap", clap(about = "Screenshot the focused window"))]
|
#[cfg_attr(feature = "clap", clap(about = "Screenshot the focused window"))]
|
||||||
ScreenshotWindow {
|
ScreenshotWindow {
|
||||||
@@ -172,7 +235,14 @@ pub enum Action {
|
|||||||
/// If `None`, uses the focused window.
|
/// If `None`, uses the focused window.
|
||||||
#[cfg_attr(feature = "clap", arg(long))]
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
id: Option<u64>,
|
id: Option<u64>,
|
||||||
|
/// Write the screenshot to disk in addition to putting it in your clipboard.
|
||||||
|
///
|
||||||
|
/// The screenshot is saved according to the `screenshot-path` config setting.
|
||||||
|
#[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||||
|
write_to_disk: bool,
|
||||||
},
|
},
|
||||||
|
/// Enable or disable the keyboard shortcuts inhibitor (if any) for the focused surface.
|
||||||
|
ToggleKeyboardShortcutsInhibit {},
|
||||||
/// Close a window.
|
/// Close a window.
|
||||||
#[cfg_attr(feature = "clap", clap(about = "Close the focused window"))]
|
#[cfg_attr(feature = "clap", clap(about = "Close the focused window"))]
|
||||||
CloseWindow {
|
CloseWindow {
|
||||||
@@ -194,12 +264,34 @@ pub enum Action {
|
|||||||
#[cfg_attr(feature = "clap", arg(long))]
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
id: Option<u64>,
|
id: Option<u64>,
|
||||||
},
|
},
|
||||||
|
/// Toggle windowed (fake) fullscreen on a window.
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "clap",
|
||||||
|
clap(about = "Toggle windowed (fake) fullscreen on the focused window")
|
||||||
|
)]
|
||||||
|
ToggleWindowedFullscreen {
|
||||||
|
/// Id of the window to toggle windowed fullscreen of.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused window.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
id: Option<u64>,
|
||||||
|
},
|
||||||
/// Focus a window by id.
|
/// Focus a window by id.
|
||||||
FocusWindow {
|
FocusWindow {
|
||||||
/// Id of the window to focus.
|
/// Id of the window to focus.
|
||||||
#[cfg_attr(feature = "clap", arg(long))]
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
id: u64,
|
id: u64,
|
||||||
},
|
},
|
||||||
|
/// Focus a window in the focused column by index.
|
||||||
|
FocusWindowInColumn {
|
||||||
|
/// Index of the window in the column.
|
||||||
|
///
|
||||||
|
/// The index starts from 1 for the topmost window.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
index: u8,
|
||||||
|
},
|
||||||
|
/// Focus the previously focused window.
|
||||||
|
FocusWindowPrevious {},
|
||||||
/// Focus the column to the left.
|
/// Focus the column to the left.
|
||||||
FocusColumnLeft {},
|
FocusColumnLeft {},
|
||||||
/// Focus the column to the right.
|
/// Focus the column to the right.
|
||||||
@@ -212,6 +304,14 @@ pub enum Action {
|
|||||||
FocusColumnRightOrFirst {},
|
FocusColumnRightOrFirst {},
|
||||||
/// Focus the next column to the left, looping if at start.
|
/// Focus the next column to the left, looping if at start.
|
||||||
FocusColumnLeftOrLast {},
|
FocusColumnLeftOrLast {},
|
||||||
|
/// Focus a column by index.
|
||||||
|
FocusColumn {
|
||||||
|
/// Index of the column to focus.
|
||||||
|
///
|
||||||
|
/// The index starts from 1 for the first column.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
index: usize,
|
||||||
|
},
|
||||||
/// Focus the window or the monitor above.
|
/// Focus the window or the monitor above.
|
||||||
FocusWindowOrMonitorUp {},
|
FocusWindowOrMonitorUp {},
|
||||||
/// Focus the window or the monitor below.
|
/// Focus the window or the monitor below.
|
||||||
@@ -236,6 +336,14 @@ pub enum Action {
|
|||||||
FocusWindowOrWorkspaceDown {},
|
FocusWindowOrWorkspaceDown {},
|
||||||
/// Focus the window or the workspace above.
|
/// Focus the window or the workspace above.
|
||||||
FocusWindowOrWorkspaceUp {},
|
FocusWindowOrWorkspaceUp {},
|
||||||
|
/// Focus the topmost window.
|
||||||
|
FocusWindowTop {},
|
||||||
|
/// Focus the bottommost window.
|
||||||
|
FocusWindowBottom {},
|
||||||
|
/// Focus the window below or the topmost window.
|
||||||
|
FocusWindowDownOrTop {},
|
||||||
|
/// Focus the window above or the bottommost window.
|
||||||
|
FocusWindowUpOrBottom {},
|
||||||
/// Move the focused column to the left.
|
/// Move the focused column to the left.
|
||||||
MoveColumnLeft {},
|
MoveColumnLeft {},
|
||||||
/// Move the focused column to the right.
|
/// Move the focused column to the right.
|
||||||
@@ -248,6 +356,14 @@ pub enum Action {
|
|||||||
MoveColumnLeftOrToMonitorLeft {},
|
MoveColumnLeftOrToMonitorLeft {},
|
||||||
/// Move the focused column to the right or to the monitor to the right.
|
/// Move the focused column to the right or to the monitor to the right.
|
||||||
MoveColumnRightOrToMonitorRight {},
|
MoveColumnRightOrToMonitorRight {},
|
||||||
|
/// Move the focused column to a specific index on its workspace.
|
||||||
|
MoveColumnToIndex {
|
||||||
|
/// New index for the column.
|
||||||
|
///
|
||||||
|
/// The index starts from 1 for the first column.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
index: usize,
|
||||||
|
},
|
||||||
/// Move the focused window down in a column.
|
/// Move the focused window down in a column.
|
||||||
MoveWindowDown {},
|
MoveWindowDown {},
|
||||||
/// Move the focused window up in a column.
|
/// Move the focused window up in a column.
|
||||||
@@ -284,8 +400,34 @@ pub enum Action {
|
|||||||
ConsumeWindowIntoColumn {},
|
ConsumeWindowIntoColumn {},
|
||||||
/// Expel the focused window from the column.
|
/// Expel the focused window from the column.
|
||||||
ExpelWindowFromColumn {},
|
ExpelWindowFromColumn {},
|
||||||
|
/// Swap focused window with one to the right.
|
||||||
|
SwapWindowRight {},
|
||||||
|
/// Swap focused window with one to the left.
|
||||||
|
SwapWindowLeft {},
|
||||||
|
/// Toggle the focused column between normal and tabbed display.
|
||||||
|
ToggleColumnTabbedDisplay {},
|
||||||
|
/// Set the display mode of the focused column.
|
||||||
|
SetColumnDisplay {
|
||||||
|
/// Display mode to set.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
display: ColumnDisplay,
|
||||||
|
},
|
||||||
/// Center the focused column on the screen.
|
/// Center the focused column on the screen.
|
||||||
CenterColumn {},
|
CenterColumn {},
|
||||||
|
/// Center a window on the screen.
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "clap",
|
||||||
|
clap(about = "Center the focused window on the screen")
|
||||||
|
)]
|
||||||
|
CenterWindow {
|
||||||
|
/// Id of the window to center.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused window.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
id: Option<u64>,
|
||||||
|
},
|
||||||
|
/// Center all fully visible columns on the screen.
|
||||||
|
CenterVisibleColumns {},
|
||||||
/// Focus the workspace below.
|
/// Focus the workspace below.
|
||||||
FocusWorkspaceDown {},
|
FocusWorkspaceDown {},
|
||||||
/// Focus the workspace above.
|
/// Focus the workspace above.
|
||||||
@@ -317,21 +459,94 @@ pub enum Action {
|
|||||||
/// Reference (index or name) of the workspace to move the window to.
|
/// Reference (index or name) of the workspace to move the window to.
|
||||||
#[cfg_attr(feature = "clap", arg())]
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
reference: WorkspaceReferenceArg,
|
reference: WorkspaceReferenceArg,
|
||||||
|
|
||||||
|
/// Whether the focus should follow the moved window.
|
||||||
|
///
|
||||||
|
/// If `true` (the default) and the window to move is focused, the focus will follow the
|
||||||
|
/// window to the new workspace. If `false`, the focus will remain on the original
|
||||||
|
/// workspace.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||||
|
focus: bool,
|
||||||
},
|
},
|
||||||
/// Move the focused column to the workspace below.
|
/// Move the focused column to the workspace below.
|
||||||
MoveColumnToWorkspaceDown {},
|
MoveColumnToWorkspaceDown {
|
||||||
|
/// Whether the focus should follow the target workspace.
|
||||||
|
///
|
||||||
|
/// If `true` (the default), the focus will follow the column to the new workspace. If
|
||||||
|
/// `false`, the focus will remain on the original workspace.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||||
|
focus: bool,
|
||||||
|
},
|
||||||
/// Move the focused column to the workspace above.
|
/// Move the focused column to the workspace above.
|
||||||
MoveColumnToWorkspaceUp {},
|
MoveColumnToWorkspaceUp {
|
||||||
|
/// Whether the focus should follow the target workspace.
|
||||||
|
///
|
||||||
|
/// If `true` (the default), the focus will follow the column to the new workspace. If
|
||||||
|
/// `false`, the focus will remain on the original workspace.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||||
|
focus: bool,
|
||||||
|
},
|
||||||
/// Move the focused column to a workspace by reference (index or name).
|
/// Move the focused column to a workspace by reference (index or name).
|
||||||
MoveColumnToWorkspace {
|
MoveColumnToWorkspace {
|
||||||
/// Reference (index or name) of the workspace to move the column to.
|
/// Reference (index or name) of the workspace to move the column to.
|
||||||
#[cfg_attr(feature = "clap", arg())]
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
reference: WorkspaceReferenceArg,
|
reference: WorkspaceReferenceArg,
|
||||||
|
|
||||||
|
/// Whether the focus should follow the target workspace.
|
||||||
|
///
|
||||||
|
/// If `true` (the default), the focus will follow the column to the new workspace. If
|
||||||
|
/// `false`, the focus will remain on the original workspace.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||||
|
focus: bool,
|
||||||
},
|
},
|
||||||
/// Move the focused workspace down.
|
/// Move the focused workspace down.
|
||||||
MoveWorkspaceDown {},
|
MoveWorkspaceDown {},
|
||||||
/// Move the focused workspace up.
|
/// Move the focused workspace up.
|
||||||
MoveWorkspaceUp {},
|
MoveWorkspaceUp {},
|
||||||
|
/// Move a workspace to a specific index on its monitor.
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "clap",
|
||||||
|
clap(about = "Move the focused workspace to a specific index on its monitor")
|
||||||
|
)]
|
||||||
|
MoveWorkspaceToIndex {
|
||||||
|
/// New index for the workspace.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
index: usize,
|
||||||
|
|
||||||
|
/// Reference (index or name) of the workspace to move.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused workspace.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
reference: Option<WorkspaceReferenceArg>,
|
||||||
|
},
|
||||||
|
/// Set the name of a workspace.
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "clap",
|
||||||
|
clap(about = "Set the name of the focused workspace")
|
||||||
|
)]
|
||||||
|
SetWorkspaceName {
|
||||||
|
/// New name for the workspace.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
/// Reference (index or name) of the workspace to name.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused workspace.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
workspace: Option<WorkspaceReferenceArg>,
|
||||||
|
},
|
||||||
|
/// Unset the name of a workspace.
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "clap",
|
||||||
|
clap(about = "Unset the name of the focused workspace")
|
||||||
|
)]
|
||||||
|
UnsetWorkspaceName {
|
||||||
|
/// Reference (index or name) of the workspace to unname.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused workspace.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
reference: Option<WorkspaceReferenceArg>,
|
||||||
|
},
|
||||||
/// Focus the monitor to the left.
|
/// Focus the monitor to the left.
|
||||||
FocusMonitorLeft {},
|
FocusMonitorLeft {},
|
||||||
/// Focus the monitor to the right.
|
/// Focus the monitor to the right.
|
||||||
@@ -340,6 +555,16 @@ pub enum Action {
|
|||||||
FocusMonitorDown {},
|
FocusMonitorDown {},
|
||||||
/// Focus the monitor above.
|
/// Focus the monitor above.
|
||||||
FocusMonitorUp {},
|
FocusMonitorUp {},
|
||||||
|
/// Focus the previous monitor.
|
||||||
|
FocusMonitorPrevious {},
|
||||||
|
/// Focus the next monitor.
|
||||||
|
FocusMonitorNext {},
|
||||||
|
/// Focus a monitor by name.
|
||||||
|
FocusMonitor {
|
||||||
|
/// Name of the output to focus.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
output: String,
|
||||||
|
},
|
||||||
/// Move the focused window to the monitor to the left.
|
/// Move the focused window to the monitor to the left.
|
||||||
MoveWindowToMonitorLeft {},
|
MoveWindowToMonitorLeft {},
|
||||||
/// Move the focused window to the monitor to the right.
|
/// Move the focused window to the monitor to the right.
|
||||||
@@ -348,6 +573,26 @@ pub enum Action {
|
|||||||
MoveWindowToMonitorDown {},
|
MoveWindowToMonitorDown {},
|
||||||
/// Move the focused window to the monitor above.
|
/// Move the focused window to the monitor above.
|
||||||
MoveWindowToMonitorUp {},
|
MoveWindowToMonitorUp {},
|
||||||
|
/// Move the focused window to the previous monitor.
|
||||||
|
MoveWindowToMonitorPrevious {},
|
||||||
|
/// Move the focused window to the next monitor.
|
||||||
|
MoveWindowToMonitorNext {},
|
||||||
|
/// Move a window to a specific monitor.
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "clap",
|
||||||
|
clap(about = "Move the focused window to a specific monitor")
|
||||||
|
)]
|
||||||
|
MoveWindowToMonitor {
|
||||||
|
/// Id of the window to move.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused window.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
id: Option<u64>,
|
||||||
|
|
||||||
|
/// The target output name.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
output: String,
|
||||||
|
},
|
||||||
/// Move the focused column to the monitor to the left.
|
/// Move the focused column to the monitor to the left.
|
||||||
MoveColumnToMonitorLeft {},
|
MoveColumnToMonitorLeft {},
|
||||||
/// Move the focused column to the monitor to the right.
|
/// Move the focused column to the monitor to the right.
|
||||||
@@ -356,6 +601,32 @@ pub enum Action {
|
|||||||
MoveColumnToMonitorDown {},
|
MoveColumnToMonitorDown {},
|
||||||
/// Move the focused column to the monitor above.
|
/// Move the focused column to the monitor above.
|
||||||
MoveColumnToMonitorUp {},
|
MoveColumnToMonitorUp {},
|
||||||
|
/// Move the focused column to the previous monitor.
|
||||||
|
MoveColumnToMonitorPrevious {},
|
||||||
|
/// Move the focused column to the next monitor.
|
||||||
|
MoveColumnToMonitorNext {},
|
||||||
|
/// Move the focused column to a specific monitor.
|
||||||
|
MoveColumnToMonitor {
|
||||||
|
/// The target output name.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
output: String,
|
||||||
|
},
|
||||||
|
/// Change the width of a window.
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "clap",
|
||||||
|
clap(about = "Change the width of the focused window")
|
||||||
|
)]
|
||||||
|
SetWindowWidth {
|
||||||
|
/// Id of the window whose width to set.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused window.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
id: Option<u64>,
|
||||||
|
|
||||||
|
/// How to change the width.
|
||||||
|
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
|
||||||
|
change: SizeChange,
|
||||||
|
},
|
||||||
/// Change the height of a window.
|
/// Change the height of a window.
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "clap",
|
feature = "clap",
|
||||||
@@ -369,7 +640,7 @@ pub enum Action {
|
|||||||
id: Option<u64>,
|
id: Option<u64>,
|
||||||
|
|
||||||
/// How to change the height.
|
/// How to change the height.
|
||||||
#[cfg_attr(feature = "clap", arg())]
|
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
|
||||||
change: SizeChange,
|
change: SizeChange,
|
||||||
},
|
},
|
||||||
/// Reset the height of a window back to automatic.
|
/// Reset the height of a window back to automatic.
|
||||||
@@ -386,6 +657,14 @@ pub enum Action {
|
|||||||
},
|
},
|
||||||
/// Switch between preset column widths.
|
/// Switch between preset column widths.
|
||||||
SwitchPresetColumnWidth {},
|
SwitchPresetColumnWidth {},
|
||||||
|
/// Switch between preset window widths.
|
||||||
|
SwitchPresetWindowWidth {
|
||||||
|
/// Id of the window whose width to switch.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused window.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
id: Option<u64>,
|
||||||
|
},
|
||||||
/// Switch between preset window heights.
|
/// Switch between preset window heights.
|
||||||
SwitchPresetWindowHeight {
|
SwitchPresetWindowHeight {
|
||||||
/// Id of the window whose height to switch.
|
/// Id of the window whose height to switch.
|
||||||
@@ -399,9 +678,11 @@ pub enum Action {
|
|||||||
/// Change the width of the focused column.
|
/// Change the width of the focused column.
|
||||||
SetColumnWidth {
|
SetColumnWidth {
|
||||||
/// How to change the width.
|
/// How to change the width.
|
||||||
#[cfg_attr(feature = "clap", arg())]
|
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
|
||||||
change: SizeChange,
|
change: SizeChange,
|
||||||
},
|
},
|
||||||
|
/// Expand the focused column to space not taken up by other fully visible columns.
|
||||||
|
ExpandColumnToAvailableWidth {},
|
||||||
/// Switch between keyboard layouts.
|
/// Switch between keyboard layouts.
|
||||||
SwitchLayout {
|
SwitchLayout {
|
||||||
/// Layout to switch to.
|
/// Layout to switch to.
|
||||||
@@ -418,12 +699,147 @@ pub enum Action {
|
|||||||
MoveWorkspaceToMonitorDown {},
|
MoveWorkspaceToMonitorDown {},
|
||||||
/// Move the focused workspace to the monitor above.
|
/// Move the focused workspace to the monitor above.
|
||||||
MoveWorkspaceToMonitorUp {},
|
MoveWorkspaceToMonitorUp {},
|
||||||
|
/// Move the focused workspace to the previous monitor.
|
||||||
|
MoveWorkspaceToMonitorPrevious {},
|
||||||
|
/// Move the focused workspace to the next monitor.
|
||||||
|
MoveWorkspaceToMonitorNext {},
|
||||||
|
/// Move a workspace to a specific monitor.
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "clap",
|
||||||
|
clap(about = "Move the focused workspace to a specific monitor")
|
||||||
|
)]
|
||||||
|
MoveWorkspaceToMonitor {
|
||||||
|
/// The target output name.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
output: String,
|
||||||
|
|
||||||
|
// Reference (index or name) of the workspace to move.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused workspace.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
reference: Option<WorkspaceReferenceArg>,
|
||||||
|
},
|
||||||
/// Toggle a debug tint on windows.
|
/// Toggle a debug tint on windows.
|
||||||
ToggleDebugTint {},
|
ToggleDebugTint {},
|
||||||
/// Toggle visualization of render element opaque regions.
|
/// Toggle visualization of render element opaque regions.
|
||||||
DebugToggleOpaqueRegions {},
|
DebugToggleOpaqueRegions {},
|
||||||
/// Toggle visualization of output damage.
|
/// Toggle visualization of output damage.
|
||||||
DebugToggleDamage {},
|
DebugToggleDamage {},
|
||||||
|
/// Move the focused window between the floating and the tiling layout.
|
||||||
|
ToggleWindowFloating {
|
||||||
|
/// Id of the window to move.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused window.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
id: Option<u64>,
|
||||||
|
},
|
||||||
|
/// Move the focused window to the floating layout.
|
||||||
|
MoveWindowToFloating {
|
||||||
|
/// Id of the window to move.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused window.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
id: Option<u64>,
|
||||||
|
},
|
||||||
|
/// Move the focused window to the tiling layout.
|
||||||
|
MoveWindowToTiling {
|
||||||
|
/// Id of the window to move.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused window.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
id: Option<u64>,
|
||||||
|
},
|
||||||
|
/// Switches focus to the floating layout.
|
||||||
|
FocusFloating {},
|
||||||
|
/// Switches focus to the tiling layout.
|
||||||
|
FocusTiling {},
|
||||||
|
/// Toggles the focus between the floating and the tiling layout.
|
||||||
|
SwitchFocusBetweenFloatingAndTiling {},
|
||||||
|
/// Move a floating window on screen.
|
||||||
|
#[cfg_attr(feature = "clap", clap(about = "Move the floating window on screen"))]
|
||||||
|
MoveFloatingWindow {
|
||||||
|
/// Id of the window to move.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused window.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
id: Option<u64>,
|
||||||
|
|
||||||
|
/// How to change the X position.
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "clap",
|
||||||
|
arg(short, long, default_value = "+0", allow_negative_numbers = true)
|
||||||
|
)]
|
||||||
|
x: PositionChange,
|
||||||
|
|
||||||
|
/// How to change the Y position.
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "clap",
|
||||||
|
arg(short, long, default_value = "+0", allow_negative_numbers = true)
|
||||||
|
)]
|
||||||
|
y: PositionChange,
|
||||||
|
},
|
||||||
|
/// Toggle the opacity of a window.
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "clap",
|
||||||
|
clap(about = "Toggle the opacity of the focused window")
|
||||||
|
)]
|
||||||
|
ToggleWindowRuleOpacity {
|
||||||
|
/// Id of the window.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused window.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
id: Option<u64>,
|
||||||
|
},
|
||||||
|
/// Set the dynamic cast target to a window.
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "clap",
|
||||||
|
clap(about = "Set the dynamic cast target to the focused window")
|
||||||
|
)]
|
||||||
|
SetDynamicCastWindow {
|
||||||
|
/// Id of the window to target.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused window.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
id: Option<u64>,
|
||||||
|
},
|
||||||
|
/// Set the dynamic cast target to a monitor.
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "clap",
|
||||||
|
clap(about = "Set the dynamic cast target to the focused monitor")
|
||||||
|
)]
|
||||||
|
SetDynamicCastMonitor {
|
||||||
|
/// Name of the output to target.
|
||||||
|
///
|
||||||
|
/// If `None`, uses the focused output.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
output: Option<String>,
|
||||||
|
},
|
||||||
|
/// Clear the dynamic cast target, making it show nothing.
|
||||||
|
ClearDynamicCastTarget {},
|
||||||
|
/// Toggle (open/close) the Overview.
|
||||||
|
ToggleOverview {},
|
||||||
|
/// Open the Overview.
|
||||||
|
OpenOverview {},
|
||||||
|
/// Close the Overview.
|
||||||
|
CloseOverview {},
|
||||||
|
/// Toggle urgent status of a window.
|
||||||
|
ToggleWindowUrgent {
|
||||||
|
/// Id of the window to toggle urgent.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
id: u64,
|
||||||
|
},
|
||||||
|
/// Set urgent status of a window.
|
||||||
|
SetWindowUrgent {
|
||||||
|
/// Id of the window to set urgent.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
id: u64,
|
||||||
|
},
|
||||||
|
/// Unset urgent status of a window.
|
||||||
|
UnsetWindowUrgent {
|
||||||
|
/// Id of the window to unset urgent.
|
||||||
|
#[cfg_attr(feature = "clap", arg(long))]
|
||||||
|
id: u64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change in window or column size.
|
/// Change in window or column size.
|
||||||
@@ -440,6 +856,16 @@ pub enum SizeChange {
|
|||||||
AdjustProportion(f64),
|
AdjustProportion(f64),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Change in floating window position.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
pub enum PositionChange {
|
||||||
|
/// Set the position in logical pixels.
|
||||||
|
SetFixed(f64),
|
||||||
|
/// Add or subtract to the current position in logical pixels.
|
||||||
|
AdjustFixed(f64),
|
||||||
|
}
|
||||||
|
|
||||||
/// Workspace reference (id, index or name) to operate on.
|
/// Workspace reference (id, index or name) to operate on.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
@@ -460,6 +886,18 @@ pub enum LayoutSwitchTarget {
|
|||||||
Next,
|
Next,
|
||||||
/// The previous configured layout.
|
/// The previous configured layout.
|
||||||
Prev,
|
Prev,
|
||||||
|
/// The specific layout by index.
|
||||||
|
Index(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How windows display in a column.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
pub enum ColumnDisplay {
|
||||||
|
/// Windows are tiled vertically across the working area height.
|
||||||
|
Normal,
|
||||||
|
/// Windows are in tabs.
|
||||||
|
Tabbed,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Output actions that niri can perform.
|
/// Output actions that niri can perform.
|
||||||
@@ -695,12 +1133,23 @@ pub struct Window {
|
|||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
/// Application ID, if set.
|
/// Application ID, if set.
|
||||||
pub app_id: Option<String>,
|
pub app_id: Option<String>,
|
||||||
|
/// Process ID that created the Wayland connection for this window, if known.
|
||||||
|
///
|
||||||
|
/// Currently, windows created by xdg-desktop-portal-gnome will have a `None` PID, but this may
|
||||||
|
/// change in the future.
|
||||||
|
pub pid: Option<i32>,
|
||||||
/// Id of the workspace this window is on, if any.
|
/// Id of the workspace this window is on, if any.
|
||||||
pub workspace_id: Option<u64>,
|
pub workspace_id: Option<u64>,
|
||||||
/// Whether this window is currently focused.
|
/// Whether this window is currently focused.
|
||||||
///
|
///
|
||||||
/// There can be either one focused window or zero (e.g. when a layer-shell surface has focus).
|
/// There can be either one focused window or zero (e.g. when a layer-shell surface has focus).
|
||||||
pub is_focused: bool,
|
pub is_focused: bool,
|
||||||
|
/// Whether this window is currently floating.
|
||||||
|
///
|
||||||
|
/// If the window isn't floating then it is in the tiling layout.
|
||||||
|
pub is_floating: bool,
|
||||||
|
/// Whether this window requests your attention.
|
||||||
|
pub is_urgent: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Output configuration change result.
|
/// Output configuration change result.
|
||||||
@@ -740,6 +1189,8 @@ pub struct Workspace {
|
|||||||
///
|
///
|
||||||
/// Can be `None` if no outputs are currently connected.
|
/// Can be `None` if no outputs are currently connected.
|
||||||
pub output: Option<String>,
|
pub output: Option<String>,
|
||||||
|
/// Whether the workspace currently has an urgent window in its output.
|
||||||
|
pub is_urgent: bool,
|
||||||
/// Whether the workspace is currently active on its output.
|
/// Whether the workspace is currently active on its output.
|
||||||
///
|
///
|
||||||
/// Every output has one active workspace, the one that is currently visible on that output.
|
/// Every output has one active workspace, the one that is currently visible on that output.
|
||||||
@@ -762,6 +1213,46 @@ pub struct KeyboardLayouts {
|
|||||||
pub current_idx: u8,
|
pub current_idx: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A layer-shell layer.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
pub enum Layer {
|
||||||
|
/// The background layer.
|
||||||
|
Background,
|
||||||
|
/// The bottom layer.
|
||||||
|
Bottom,
|
||||||
|
/// The top layer.
|
||||||
|
Top,
|
||||||
|
/// The overlay layer.
|
||||||
|
Overlay,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keyboard interactivity modes for a layer-shell surface.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
pub enum LayerSurfaceKeyboardInteractivity {
|
||||||
|
/// Surface cannot receive keyboard focus.
|
||||||
|
None,
|
||||||
|
/// Surface receives keyboard focus whenever possible.
|
||||||
|
Exclusive,
|
||||||
|
/// Surface receives keyboard focus on demand, e.g. when clicked.
|
||||||
|
OnDemand,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A layer-shell surface.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
pub struct LayerSurface {
|
||||||
|
/// Namespace provided by the layer-shell client.
|
||||||
|
pub namespace: String,
|
||||||
|
/// Name of the output the surface is on.
|
||||||
|
pub output: String,
|
||||||
|
/// Layer that the surface is on.
|
||||||
|
pub layer: Layer,
|
||||||
|
/// The surface's keyboard interactivity mode.
|
||||||
|
pub keyboard_interactivity: LayerSurfaceKeyboardInteractivity,
|
||||||
|
}
|
||||||
|
|
||||||
/// A compositor event.
|
/// A compositor event.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
@@ -774,6 +1265,13 @@ pub enum Event {
|
|||||||
/// workspaces are missing from here, then they were deleted.
|
/// workspaces are missing from here, then they were deleted.
|
||||||
workspaces: Vec<Workspace>,
|
workspaces: Vec<Workspace>,
|
||||||
},
|
},
|
||||||
|
/// The workspace urgency changed.
|
||||||
|
WorkspaceUrgencyChanged {
|
||||||
|
/// Id of the workspace.
|
||||||
|
id: u64,
|
||||||
|
/// Whether this workspace has an urgent window.
|
||||||
|
urgent: bool,
|
||||||
|
},
|
||||||
/// A workspace was activated on an output.
|
/// A workspace was activated on an output.
|
||||||
///
|
///
|
||||||
/// This doesn't always mean the workspace became focused, just that it's now the active
|
/// This doesn't always mean the workspace became focused, just that it's now the active
|
||||||
@@ -821,6 +1319,13 @@ pub enum Event {
|
|||||||
/// Id of the newly focused window, or `None` if no window is now focused.
|
/// Id of the newly focused window, or `None` if no window is now focused.
|
||||||
id: Option<u64>,
|
id: Option<u64>,
|
||||||
},
|
},
|
||||||
|
/// Window urgency changed.
|
||||||
|
WindowUrgencyChanged {
|
||||||
|
/// Id of the window.
|
||||||
|
id: u64,
|
||||||
|
/// The new urgency state of the window.
|
||||||
|
urgent: bool,
|
||||||
|
},
|
||||||
/// The configured keyboard layouts have changed.
|
/// The configured keyboard layouts have changed.
|
||||||
KeyboardLayoutsChanged {
|
KeyboardLayoutsChanged {
|
||||||
/// The new keyboard layout configuration.
|
/// The new keyboard layout configuration.
|
||||||
@@ -831,6 +1336,11 @@ pub enum Event {
|
|||||||
/// Index of the newly active layout.
|
/// Index of the newly active layout.
|
||||||
idx: u8,
|
idx: u8,
|
||||||
},
|
},
|
||||||
|
/// The overview was opened or closed.
|
||||||
|
OverviewOpenedOrClosed {
|
||||||
|
/// The new state of the overview.
|
||||||
|
is_open: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for WorkspaceReferenceArg {
|
impl FromStr for WorkspaceReferenceArg {
|
||||||
@@ -891,6 +1401,25 @@ impl FromStr for SizeChange {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromStr for PositionChange {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let value = s;
|
||||||
|
match value.bytes().next() {
|
||||||
|
Some(b'-' | b'+') => {
|
||||||
|
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||||
|
Ok(Self::AdjustFixed(value))
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||||
|
Ok(Self::SetFixed(value))
|
||||||
|
}
|
||||||
|
None => Err("value is missing"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FromStr for LayoutSwitchTarget {
|
impl FromStr for LayoutSwitchTarget {
|
||||||
type Err = &'static str;
|
type Err = &'static str;
|
||||||
|
|
||||||
@@ -898,7 +1427,22 @@ impl FromStr for LayoutSwitchTarget {
|
|||||||
match s {
|
match s {
|
||||||
"next" => Ok(Self::Next),
|
"next" => Ok(Self::Next),
|
||||||
"prev" => Ok(Self::Prev),
|
"prev" => Ok(Self::Prev),
|
||||||
_ => Err(r#"invalid layout action, can be "next" or "prev""#),
|
other => match other.parse() {
|
||||||
|
Ok(layout) => Ok(Self::Index(layout)),
|
||||||
|
_ => Err(r#"invalid layout action, can be "next", "prev" or a layout index"#),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ColumnDisplay {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"normal" => Ok(Self::Normal),
|
||||||
|
"tabbed" => Ok(Self::Tabbed),
|
||||||
|
_ => Err(r#"invalid column display, can be "normal" or "tabbed""#),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-18
@@ -16,7 +16,7 @@ pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
|
|||||||
/// This struct is used to communicate with the niri IPC server. It handles the socket connection
|
/// This struct is used to communicate with the niri IPC server. It handles the socket connection
|
||||||
/// and serialization/deserialization of messages.
|
/// and serialization/deserialization of messages.
|
||||||
pub struct Socket {
|
pub struct Socket {
|
||||||
stream: UnixStream,
|
stream: BufReader<UnixStream>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Socket {
|
impl Socket {
|
||||||
@@ -37,6 +37,7 @@ impl Socket {
|
|||||||
/// Connects to the niri IPC socket at the given path.
|
/// Connects to the niri IPC socket at the given path.
|
||||||
pub fn connect_to(path: impl AsRef<Path>) -> io::Result<Self> {
|
pub fn connect_to(path: impl AsRef<Path>) -> io::Result<Self> {
|
||||||
let stream = UnixStream::connect(path.as_ref())?;
|
let stream = UnixStream::connect(path.as_ref())?;
|
||||||
|
let stream = BufReader::new(stream);
|
||||||
Ok(Self { stream })
|
Ok(Self { stream })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,31 +48,54 @@ impl Socket {
|
|||||||
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
|
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
|
||||||
/// * `Ok(Err(message))`: error message from niri
|
/// * `Ok(Err(message))`: error message from niri
|
||||||
/// * `Err(error)`: error communicating with niri
|
/// * `Err(error)`: error communicating with niri
|
||||||
///
|
pub fn send(&mut self, request: Request) -> io::Result<Reply> {
|
||||||
/// 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();
|
let mut buf = serde_json::to_string(&request).unwrap();
|
||||||
stream.write_all(buf.as_bytes())?;
|
buf.push('\n');
|
||||||
stream.shutdown(Shutdown::Write)?;
|
self.stream.get_mut().write_all(buf.as_bytes())?;
|
||||||
|
|
||||||
let mut reader = BufReader::new(stream);
|
|
||||||
|
|
||||||
buf.clear();
|
buf.clear();
|
||||||
reader.read_line(&mut buf)?;
|
self.stream.read_line(&mut buf)?;
|
||||||
|
|
||||||
let reply = serde_json::from_str(&buf)?;
|
let reply = serde_json::from_str(&buf)?;
|
||||||
|
Ok(reply)
|
||||||
|
}
|
||||||
|
|
||||||
let events = move || {
|
/// Starts reading event stream [`Event`]s from the socket.
|
||||||
|
///
|
||||||
|
/// The returned function will block until the next [`Event`] arrives, then return it.
|
||||||
|
///
|
||||||
|
/// Use this only after requesting an [`EventStream`][Request::EventStream].
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use niri_ipc::{Request, Response};
|
||||||
|
/// use niri_ipc::socket::Socket;
|
||||||
|
///
|
||||||
|
/// fn main() -> std::io::Result<()> {
|
||||||
|
/// let mut socket = Socket::connect()?;
|
||||||
|
///
|
||||||
|
/// let reply = socket.send(Request::EventStream)?;
|
||||||
|
/// if matches!(reply, Ok(Response::Handled)) {
|
||||||
|
/// let mut read_event = socket.read_events();
|
||||||
|
/// while let Ok(event) = read_event() {
|
||||||
|
/// println!("Received event: {event:?}");
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn read_events(self) -> impl FnMut() -> io::Result<Event> {
|
||||||
|
let Self { mut stream } = self;
|
||||||
|
let _ = stream.get_mut().shutdown(Shutdown::Write);
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
move || {
|
||||||
buf.clear();
|
buf.clear();
|
||||||
reader.read_line(&mut buf)?;
|
stream.read_line(&mut buf)?;
|
||||||
let event = serde_json::from_str(&buf)?;
|
let event = serde_json::from_str(&buf)?;
|
||||||
Ok(event)
|
Ok(event)
|
||||||
};
|
}
|
||||||
|
|
||||||
Ok((reply, events))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ pub struct EventStreamState {
|
|||||||
|
|
||||||
/// State of the keyboard layouts.
|
/// State of the keyboard layouts.
|
||||||
pub keyboard_layouts: KeyboardLayoutsState,
|
pub keyboard_layouts: KeyboardLayoutsState,
|
||||||
|
|
||||||
|
/// State of the overview.
|
||||||
|
pub overview: OverviewState,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The workspaces state communicated over the event stream.
|
/// The workspaces state communicated over the event stream.
|
||||||
@@ -63,12 +66,20 @@ pub struct KeyboardLayoutsState {
|
|||||||
pub keyboard_layouts: Option<KeyboardLayouts>,
|
pub keyboard_layouts: Option<KeyboardLayouts>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The overview state communicated over the event stream.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct OverviewState {
|
||||||
|
/// Whether the overview is currently open.
|
||||||
|
pub is_open: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl EventStreamStatePart for EventStreamState {
|
impl EventStreamStatePart for EventStreamState {
|
||||||
fn replicate(&self) -> Vec<Event> {
|
fn replicate(&self) -> Vec<Event> {
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
events.extend(self.workspaces.replicate());
|
events.extend(self.workspaces.replicate());
|
||||||
events.extend(self.windows.replicate());
|
events.extend(self.windows.replicate());
|
||||||
events.extend(self.keyboard_layouts.replicate());
|
events.extend(self.keyboard_layouts.replicate());
|
||||||
|
events.extend(self.overview.replicate());
|
||||||
events
|
events
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +87,7 @@ impl EventStreamStatePart for EventStreamState {
|
|||||||
let event = self.workspaces.apply(event)?;
|
let event = self.workspaces.apply(event)?;
|
||||||
let event = self.windows.apply(event)?;
|
let event = self.windows.apply(event)?;
|
||||||
let event = self.keyboard_layouts.apply(event)?;
|
let event = self.keyboard_layouts.apply(event)?;
|
||||||
|
let event = self.overview.apply(event)?;
|
||||||
Some(event)
|
Some(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,6 +103,13 @@ impl EventStreamStatePart for WorkspacesState {
|
|||||||
Event::WorkspacesChanged { workspaces } => {
|
Event::WorkspacesChanged { workspaces } => {
|
||||||
self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect();
|
self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect();
|
||||||
}
|
}
|
||||||
|
Event::WorkspaceUrgencyChanged { id, urgent } => {
|
||||||
|
for ws in self.workspaces.values_mut() {
|
||||||
|
if ws.id == id {
|
||||||
|
ws.is_urgent = urgent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Event::WorkspaceActivated { id, focused } => {
|
Event::WorkspaceActivated { id, focused } => {
|
||||||
let ws = self.workspaces.get(&id);
|
let ws = self.workspaces.get(&id);
|
||||||
let ws = ws.expect("activated workspace was missing from the map");
|
let ws = ws.expect("activated workspace was missing from the map");
|
||||||
@@ -162,6 +181,14 @@ impl EventStreamStatePart for WindowsState {
|
|||||||
win.is_focused = Some(win.id) == id;
|
win.is_focused = Some(win.id) == id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Event::WindowUrgencyChanged { id, urgent } => {
|
||||||
|
for win in self.windows.values_mut() {
|
||||||
|
if win.id == id {
|
||||||
|
win.is_urgent = urgent;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
event => return Some(event),
|
event => return Some(event),
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -192,3 +219,21 @@ impl EventStreamStatePart for KeyboardLayoutsState {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EventStreamStatePart for OverviewState {
|
||||||
|
fn replicate(&self) -> Vec<Event> {
|
||||||
|
vec![Event::OverviewOpenedOrClosed {
|
||||||
|
is_open: self.is_open,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||||
|
match event {
|
||||||
|
Event::OverviewOpenedOrClosed { is_open } => {
|
||||||
|
self.is_open = is_open;
|
||||||
|
}
|
||||||
|
event => return Some(event),
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ edition.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
adw = { version = "0.7.1", package = "libadwaita", features = ["v1_4"] }
|
adw = { version = "0.7.2", package = "libadwaita", features = ["v1_4"] }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
gtk = { version = "0.9.3", package = "gtk4", features = ["v4_12"] }
|
gtk = { version = "0.9.6", package = "gtk4", features = ["v4_12"] }
|
||||||
niri = { version = "0.1.10", path = ".." }
|
niri = { version = "25.5.1", path = ".." }
|
||||||
niri-config = { version = "0.1.10", path = "../niri-config" }
|
niri-config = { version = "25.5.1", path = "../niri-config" }
|
||||||
smithay.workspace = true
|
smithay.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
use std::f32::consts::{FRAC_PI_2, PI};
|
use std::f32::consts::{FRAC_PI_2, PI};
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use niri::animation::ANIMATION_SLOWDOWN;
|
|
||||||
use niri::render_helpers::border::BorderRenderElement;
|
use niri::render_helpers::border::BorderRenderElement;
|
||||||
use niri_config::{Color, CornerRadius, GradientInterpolation};
|
use niri_config::{Color, CornerRadius, GradientInterpolation};
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use super::TestCase;
|
use super::{Args, TestCase};
|
||||||
|
|
||||||
pub struct GradientAngle {
|
pub struct GradientAngle {
|
||||||
angle: f32,
|
angle: f32,
|
||||||
@@ -17,7 +15,7 @@ pub struct GradientAngle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GradientAngle {
|
impl GradientAngle {
|
||||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
pub fn new(_args: Args) -> Self {
|
||||||
Self {
|
Self {
|
||||||
angle: 0.,
|
angle: 0.,
|
||||||
prev_time: Duration::ZERO,
|
prev_time: Duration::ZERO,
|
||||||
@@ -31,20 +29,13 @@ impl TestCase for GradientAngle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn advance_animations(&mut self, current_time: Duration) {
|
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
|
Duration::ZERO
|
||||||
} else {
|
} else {
|
||||||
current_time.saturating_sub(self.prev_time)
|
current_time.saturating_sub(self.prev_time)
|
||||||
};
|
};
|
||||||
self.prev_time = current_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;
|
self.angle += delta.as_secs_f32() * PI;
|
||||||
|
|
||||||
if self.angle >= PI * 2. {
|
if self.angle >= PI * 2. {
|
||||||
@@ -59,19 +50,20 @@ impl TestCase for GradientAngle {
|
|||||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
let (a, b) = (size.w / 4, size.h / 4);
|
let (a, b) = (size.w / 4, size.h / 4);
|
||||||
let size = (size.w - a * 2, size.h - b * 2);
|
let size = (size.w - a * 2, size.h - b * 2);
|
||||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||||
|
|
||||||
[BorderRenderElement::new(
|
[BorderRenderElement::new(
|
||||||
area.size,
|
area.size,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
GradientInterpolation::default(),
|
GradientInterpolation::default(),
|
||||||
Color::new_unpremul(1., 0., 0., 1.),
|
Color::new_unpremul(1., 0., 0., 1.),
|
||||||
Color::new_unpremul(0., 1., 0., 1.),
|
Color::new_unpremul(0., 1., 0., 1.),
|
||||||
self.angle - FRAC_PI_2,
|
self.angle - FRAC_PI_2,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
0.,
|
0.,
|
||||||
CornerRadius::default(),
|
CornerRadius::default(),
|
||||||
1.,
|
1.,
|
||||||
|
1.,
|
||||||
)
|
)
|
||||||
.with_location(area.loc)]
|
.with_location(area.loc)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
use std::f32::consts::{FRAC_PI_4, PI};
|
use std::f32::consts::{FRAC_PI_4, PI};
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use niri::animation::ANIMATION_SLOWDOWN;
|
|
||||||
use niri::layout::focus_ring::FocusRing;
|
use niri::layout::focus_ring::FocusRing;
|
||||||
use niri::render_helpers::border::BorderRenderElement;
|
use niri::render_helpers::border::BorderRenderElement;
|
||||||
use niri_config::{Color, CornerRadius, FloatOrInt, GradientInterpolation};
|
use niri_config::{Color, CornerRadius, FloatOrInt, GradientInterpolation};
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Physical, Point, Rectangle, Size};
|
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use super::TestCase;
|
use super::{Args, TestCase};
|
||||||
|
|
||||||
pub struct GradientArea {
|
pub struct GradientArea {
|
||||||
progress: f32,
|
progress: f32,
|
||||||
@@ -19,14 +17,16 @@ pub struct GradientArea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GradientArea {
|
impl GradientArea {
|
||||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
pub fn new(_args: Args) -> Self {
|
||||||
let border = FocusRing::new(niri_config::FocusRing {
|
let border = FocusRing::new(niri_config::FocusRing {
|
||||||
off: false,
|
off: false,
|
||||||
width: FloatOrInt(1.),
|
width: FloatOrInt(1.),
|
||||||
active_color: Color::from_rgba8_unpremul(255, 255, 255, 128),
|
active_color: Color::from_rgba8_unpremul(255, 255, 255, 128),
|
||||||
inactive_color: Color::default(),
|
inactive_color: Color::default(),
|
||||||
|
urgent_color: Color::default(),
|
||||||
active_gradient: None,
|
active_gradient: None,
|
||||||
inactive_gradient: None,
|
inactive_gradient: None,
|
||||||
|
urgent_gradient: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -43,20 +43,13 @@ impl TestCase for GradientArea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn advance_animations(&mut self, current_time: Duration) {
|
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
|
Duration::ZERO
|
||||||
} else {
|
} else {
|
||||||
current_time.saturating_sub(self.prev_time)
|
current_time.saturating_sub(self.prev_time)
|
||||||
};
|
};
|
||||||
self.prev_time = current_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;
|
self.progress += delta.as_secs_f32() * PI;
|
||||||
|
|
||||||
if self.progress >= PI * 2. {
|
if self.progress >= PI * 2. {
|
||||||
@@ -74,8 +67,8 @@ impl TestCase for GradientArea {
|
|||||||
let f = (self.progress.sin() + 1.) / 2.;
|
let f = (self.progress.sin() + 1.) / 2.;
|
||||||
|
|
||||||
let (a, b) = (size.w / 4, size.h / 4);
|
let (a, b) = (size.w / 4, size.h / 4);
|
||||||
let rect_size = (size.w - a * 2, size.h - b * 2);
|
let rect_size = Size::from((size.w - a * 2, size.h - b * 2));
|
||||||
let area = Rectangle::from_loc_and_size((a, b), rect_size).to_f64();
|
let area = Rectangle::new(Point::from((a, b)), rect_size).to_f64();
|
||||||
|
|
||||||
let g_size = Size::from((
|
let g_size = Size::from((
|
||||||
(size.w as f32 / 8. + size.w as f32 / 8. * 7. * f).round() as i32,
|
(size.w as f32 / 8. + size.w as f32 / 8. * 7. * f).round() as i32,
|
||||||
@@ -83,16 +76,18 @@ impl TestCase for GradientArea {
|
|||||||
));
|
));
|
||||||
let g_loc = Point::from(((size.w - g_size.w) / 2, (size.h - g_size.h) / 2)).to_f64();
|
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 g_size = g_size.to_f64();
|
||||||
let mut g_area = Rectangle::from_loc_and_size(g_loc, g_size);
|
let mut g_area = Rectangle::new(g_loc, g_size);
|
||||||
g_area.loc -= area.loc;
|
g_area.loc -= area.loc;
|
||||||
|
|
||||||
self.border.update_render_elements(
|
self.border.update_render_elements(
|
||||||
g_size,
|
g_size,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
Rectangle::default(),
|
Rectangle::default(),
|
||||||
CornerRadius::default(),
|
CornerRadius::default(),
|
||||||
1.,
|
1.,
|
||||||
|
1.,
|
||||||
);
|
);
|
||||||
rv.extend(
|
rv.extend(
|
||||||
self.border
|
self.border
|
||||||
@@ -108,10 +103,11 @@ impl TestCase for GradientArea {
|
|||||||
Color::new_unpremul(1., 0., 0., 1.),
|
Color::new_unpremul(1., 0., 0., 1.),
|
||||||
Color::new_unpremul(0., 1., 0., 1.),
|
Color::new_unpremul(0., 1., 0., 1.),
|
||||||
FRAC_PI_4,
|
FRAC_PI_4,
|
||||||
Rectangle::from_loc_and_size((0, 0), rect_size).to_f64(),
|
Rectangle::from_size(rect_size).to_f64(),
|
||||||
0.,
|
0.,
|
||||||
CornerRadius::default(),
|
CornerRadius::default(),
|
||||||
1.,
|
1.,
|
||||||
|
1.,
|
||||||
)
|
)
|
||||||
.with_location(area.loc)]
|
.with_location(area.loc)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ use niri_config::{
|
|||||||
};
|
};
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use super::TestCase;
|
use super::{Args, TestCase};
|
||||||
|
|
||||||
pub struct GradientOklab {
|
pub struct GradientOklab {
|
||||||
gradient_format: GradientInterpolation,
|
gradient_format: GradientInterpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientOklab {
|
impl GradientOklab {
|
||||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
pub fn new(_args: Args) -> Self {
|
||||||
Self {
|
Self {
|
||||||
gradient_format: GradientInterpolation {
|
gradient_format: GradientInterpolation {
|
||||||
color_space: GradientColorSpace::Oklab,
|
color_space: GradientColorSpace::Oklab,
|
||||||
@@ -31,19 +31,20 @@ impl TestCase for GradientOklab {
|
|||||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
let (a, b) = (size.w / 6, size.h / 3);
|
let (a, b) = (size.w / 6, size.h / 3);
|
||||||
let size = (size.w - a * 2, size.h - b * 2);
|
let size = (size.w - a * 2, size.h - b * 2);
|
||||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||||
|
|
||||||
[BorderRenderElement::new(
|
[BorderRenderElement::new(
|
||||||
area.size,
|
area.size,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
self.gradient_format,
|
self.gradient_format,
|
||||||
Color::new_unpremul(1., 0., 0., 1.),
|
Color::new_unpremul(1., 0., 0., 1.),
|
||||||
Color::new_unpremul(0., 1., 0., 1.),
|
Color::new_unpremul(0., 1., 0., 1.),
|
||||||
0.,
|
0.,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
0.,
|
0.,
|
||||||
CornerRadius::default(),
|
CornerRadius::default(),
|
||||||
1.,
|
1.,
|
||||||
|
1.,
|
||||||
)
|
)
|
||||||
.with_location(area.loc)]
|
.with_location(area.loc)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ use niri::render_helpers::border::BorderRenderElement;
|
|||||||
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
|
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use super::TestCase;
|
use super::{Args, TestCase};
|
||||||
|
|
||||||
pub struct GradientOklabAlpha {
|
pub struct GradientOklabAlpha {
|
||||||
gradient_format: GradientInterpolation,
|
gradient_format: GradientInterpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientOklabAlpha {
|
impl GradientOklabAlpha {
|
||||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
pub fn new(_args: Args) -> Self {
|
||||||
Self {
|
Self {
|
||||||
gradient_format: GradientInterpolation {
|
gradient_format: GradientInterpolation {
|
||||||
color_space: GradientColorSpace::Oklab,
|
color_space: GradientColorSpace::Oklab,
|
||||||
@@ -29,19 +29,20 @@ impl TestCase for GradientOklabAlpha {
|
|||||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
let (a, b) = (size.w / 6, size.h / 3);
|
let (a, b) = (size.w / 6, size.h / 3);
|
||||||
let size = (size.w - a * 2, size.h - b * 2);
|
let size = (size.w - a * 2, size.h - b * 2);
|
||||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||||
|
|
||||||
[BorderRenderElement::new(
|
[BorderRenderElement::new(
|
||||||
area.size,
|
area.size,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
self.gradient_format,
|
self.gradient_format,
|
||||||
Color::new_unpremul(1., 0., 0., 1.),
|
Color::new_unpremul(1., 0., 0., 1.),
|
||||||
Color::new_unpremul(0., 1., 0., 0.),
|
Color::new_unpremul(0., 1., 0., 0.),
|
||||||
0.,
|
0.,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
0.,
|
0.,
|
||||||
CornerRadius::default(),
|
CornerRadius::default(),
|
||||||
1.,
|
1.,
|
||||||
|
1.,
|
||||||
)
|
)
|
||||||
.with_location(area.loc)]
|
.with_location(area.loc)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ use niri_config::{
|
|||||||
};
|
};
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use super::TestCase;
|
use super::{Args, TestCase};
|
||||||
|
|
||||||
pub struct GradientOklchAlpha {
|
pub struct GradientOklchAlpha {
|
||||||
gradient_format: GradientInterpolation,
|
gradient_format: GradientInterpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientOklchAlpha {
|
impl GradientOklchAlpha {
|
||||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
pub fn new(_args: Args) -> Self {
|
||||||
Self {
|
Self {
|
||||||
gradient_format: GradientInterpolation {
|
gradient_format: GradientInterpolation {
|
||||||
color_space: GradientColorSpace::Oklch,
|
color_space: GradientColorSpace::Oklch,
|
||||||
@@ -31,19 +31,20 @@ impl TestCase for GradientOklchAlpha {
|
|||||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
let (a, b) = (size.w / 6, size.h / 3);
|
let (a, b) = (size.w / 6, size.h / 3);
|
||||||
let size = (size.w - a * 2, size.h - b * 2);
|
let size = (size.w - a * 2, size.h - b * 2);
|
||||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||||
|
|
||||||
[BorderRenderElement::new(
|
[BorderRenderElement::new(
|
||||||
area.size,
|
area.size,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
self.gradient_format,
|
self.gradient_format,
|
||||||
Color::new_unpremul(1., 0., 0., 1.),
|
Color::new_unpremul(1., 0., 0., 1.),
|
||||||
Color::new_unpremul(0., 1., 0., 0.),
|
Color::new_unpremul(0., 1., 0., 0.),
|
||||||
0.,
|
0.,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
0.,
|
0.,
|
||||||
CornerRadius::default(),
|
CornerRadius::default(),
|
||||||
1.,
|
1.,
|
||||||
|
1.,
|
||||||
)
|
)
|
||||||
.with_location(area.loc)]
|
.with_location(area.loc)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ use niri_config::{
|
|||||||
};
|
};
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use super::TestCase;
|
use super::{Args, TestCase};
|
||||||
|
|
||||||
pub struct GradientOklchDecreasing {
|
pub struct GradientOklchDecreasing {
|
||||||
gradient_format: GradientInterpolation,
|
gradient_format: GradientInterpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientOklchDecreasing {
|
impl GradientOklchDecreasing {
|
||||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
pub fn new(_args: Args) -> Self {
|
||||||
Self {
|
Self {
|
||||||
gradient_format: GradientInterpolation {
|
gradient_format: GradientInterpolation {
|
||||||
color_space: GradientColorSpace::Oklch,
|
color_space: GradientColorSpace::Oklch,
|
||||||
@@ -31,19 +31,20 @@ impl TestCase for GradientOklchDecreasing {
|
|||||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
let (a, b) = (size.w / 6, size.h / 3);
|
let (a, b) = (size.w / 6, size.h / 3);
|
||||||
let size = (size.w - a * 2, size.h - b * 2);
|
let size = (size.w - a * 2, size.h - b * 2);
|
||||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||||
|
|
||||||
[BorderRenderElement::new(
|
[BorderRenderElement::new(
|
||||||
area.size,
|
area.size,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
self.gradient_format,
|
self.gradient_format,
|
||||||
Color::new_unpremul(1., 0., 0., 1.),
|
Color::new_unpremul(1., 0., 0., 1.),
|
||||||
Color::new_unpremul(0., 1., 0., 1.),
|
Color::new_unpremul(0., 1., 0., 1.),
|
||||||
0.,
|
0.,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
0.,
|
0.,
|
||||||
CornerRadius::default(),
|
CornerRadius::default(),
|
||||||
1.,
|
1.,
|
||||||
|
1.,
|
||||||
)
|
)
|
||||||
.with_location(area.loc)]
|
.with_location(area.loc)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ use niri_config::{
|
|||||||
};
|
};
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use super::TestCase;
|
use super::{Args, TestCase};
|
||||||
|
|
||||||
pub struct GradientOklchIncreasing {
|
pub struct GradientOklchIncreasing {
|
||||||
gradient_format: GradientInterpolation,
|
gradient_format: GradientInterpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientOklchIncreasing {
|
impl GradientOklchIncreasing {
|
||||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
pub fn new(_args: Args) -> Self {
|
||||||
Self {
|
Self {
|
||||||
gradient_format: GradientInterpolation {
|
gradient_format: GradientInterpolation {
|
||||||
color_space: GradientColorSpace::Oklch,
|
color_space: GradientColorSpace::Oklch,
|
||||||
@@ -31,19 +31,20 @@ impl TestCase for GradientOklchIncreasing {
|
|||||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
let (a, b) = (size.w / 6, size.h / 3);
|
let (a, b) = (size.w / 6, size.h / 3);
|
||||||
let size = (size.w - a * 2, size.h - b * 2);
|
let size = (size.w - a * 2, size.h - b * 2);
|
||||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||||
|
|
||||||
[BorderRenderElement::new(
|
[BorderRenderElement::new(
|
||||||
area.size,
|
area.size,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
self.gradient_format,
|
self.gradient_format,
|
||||||
Color::new_unpremul(1., 0., 0., 1.),
|
Color::new_unpremul(1., 0., 0., 1.),
|
||||||
Color::new_unpremul(0., 1., 0., 1.),
|
Color::new_unpremul(0., 1., 0., 1.),
|
||||||
0.,
|
0.,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
0.,
|
0.,
|
||||||
CornerRadius::default(),
|
CornerRadius::default(),
|
||||||
1.,
|
1.,
|
||||||
|
1.,
|
||||||
)
|
)
|
||||||
.with_location(area.loc)]
|
.with_location(area.loc)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ use niri_config::{
|
|||||||
};
|
};
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use super::TestCase;
|
use super::{Args, TestCase};
|
||||||
|
|
||||||
pub struct GradientOklchLonger {
|
pub struct GradientOklchLonger {
|
||||||
gradient_format: GradientInterpolation,
|
gradient_format: GradientInterpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientOklchLonger {
|
impl GradientOklchLonger {
|
||||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
pub fn new(_args: Args) -> Self {
|
||||||
Self {
|
Self {
|
||||||
gradient_format: GradientInterpolation {
|
gradient_format: GradientInterpolation {
|
||||||
color_space: GradientColorSpace::Oklch,
|
color_space: GradientColorSpace::Oklch,
|
||||||
@@ -31,19 +31,20 @@ impl TestCase for GradientOklchLonger {
|
|||||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
let (a, b) = (size.w / 6, size.h / 3);
|
let (a, b) = (size.w / 6, size.h / 3);
|
||||||
let size = (size.w - a * 2, size.h - b * 2);
|
let size = (size.w - a * 2, size.h - b * 2);
|
||||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||||
|
|
||||||
[BorderRenderElement::new(
|
[BorderRenderElement::new(
|
||||||
area.size,
|
area.size,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
self.gradient_format,
|
self.gradient_format,
|
||||||
Color::new_unpremul(1., 0., 0., 1.),
|
Color::new_unpremul(1., 0., 0., 1.),
|
||||||
Color::new_unpremul(0., 1., 0., 1.),
|
Color::new_unpremul(0., 1., 0., 1.),
|
||||||
0.,
|
0.,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
0.,
|
0.,
|
||||||
CornerRadius::default(),
|
CornerRadius::default(),
|
||||||
1.,
|
1.,
|
||||||
|
1.,
|
||||||
)
|
)
|
||||||
.with_location(area.loc)]
|
.with_location(area.loc)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ use niri_config::{
|
|||||||
};
|
};
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use super::TestCase;
|
use super::{Args, TestCase};
|
||||||
|
|
||||||
pub struct GradientOklchShorter {
|
pub struct GradientOklchShorter {
|
||||||
gradient_format: GradientInterpolation,
|
gradient_format: GradientInterpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientOklchShorter {
|
impl GradientOklchShorter {
|
||||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
pub fn new(_args: Args) -> Self {
|
||||||
Self {
|
Self {
|
||||||
gradient_format: GradientInterpolation {
|
gradient_format: GradientInterpolation {
|
||||||
color_space: GradientColorSpace::Oklch,
|
color_space: GradientColorSpace::Oklch,
|
||||||
@@ -31,19 +31,20 @@ impl TestCase for GradientOklchShorter {
|
|||||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
let (a, b) = (size.w / 6, size.h / 3);
|
let (a, b) = (size.w / 6, size.h / 3);
|
||||||
let size = (size.w - a * 2, size.h - b * 2);
|
let size = (size.w - a * 2, size.h - b * 2);
|
||||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||||
|
|
||||||
[BorderRenderElement::new(
|
[BorderRenderElement::new(
|
||||||
area.size,
|
area.size,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
self.gradient_format,
|
self.gradient_format,
|
||||||
Color::new_unpremul(1., 0., 0., 1.),
|
Color::new_unpremul(1., 0., 0., 1.),
|
||||||
Color::new_unpremul(0., 1., 0., 1.),
|
Color::new_unpremul(0., 1., 0., 1.),
|
||||||
0.,
|
0.,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
0.,
|
0.,
|
||||||
CornerRadius::default(),
|
CornerRadius::default(),
|
||||||
1.,
|
1.,
|
||||||
|
1.,
|
||||||
)
|
)
|
||||||
.with_location(area.loc)]
|
.with_location(area.loc)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ use niri_config::{
|
|||||||
};
|
};
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use super::TestCase;
|
use super::{Args, TestCase};
|
||||||
|
|
||||||
pub struct GradientSrgb {
|
pub struct GradientSrgb {
|
||||||
gradient_format: GradientInterpolation,
|
gradient_format: GradientInterpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientSrgb {
|
impl GradientSrgb {
|
||||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
pub fn new(_args: Args) -> Self {
|
||||||
Self {
|
Self {
|
||||||
gradient_format: GradientInterpolation {
|
gradient_format: GradientInterpolation {
|
||||||
color_space: GradientColorSpace::Srgb,
|
color_space: GradientColorSpace::Srgb,
|
||||||
@@ -31,19 +31,20 @@ impl TestCase for GradientSrgb {
|
|||||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
let (a, b) = (size.w / 6, size.h / 3);
|
let (a, b) = (size.w / 6, size.h / 3);
|
||||||
let size = (size.w - a * 2, size.h - b * 2);
|
let size = (size.w - a * 2, size.h - b * 2);
|
||||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||||
|
|
||||||
[BorderRenderElement::new(
|
[BorderRenderElement::new(
|
||||||
area.size,
|
area.size,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
self.gradient_format,
|
self.gradient_format,
|
||||||
Color::new_unpremul(1., 0., 0., 1.),
|
Color::new_unpremul(1., 0., 0., 1.),
|
||||||
Color::new_unpremul(0., 1., 0., 1.),
|
Color::new_unpremul(0., 1., 0., 1.),
|
||||||
0.,
|
0.,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
0.,
|
0.,
|
||||||
CornerRadius::default(),
|
CornerRadius::default(),
|
||||||
1.,
|
1.,
|
||||||
|
1.,
|
||||||
)
|
)
|
||||||
.with_location(area.loc)]
|
.with_location(area.loc)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ use niri::render_helpers::border::BorderRenderElement;
|
|||||||
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
|
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use super::TestCase;
|
use super::{Args, TestCase};
|
||||||
|
|
||||||
pub struct GradientSrgbAlpha {
|
pub struct GradientSrgbAlpha {
|
||||||
gradient_format: GradientInterpolation,
|
gradient_format: GradientInterpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientSrgbAlpha {
|
impl GradientSrgbAlpha {
|
||||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
pub fn new(_args: Args) -> Self {
|
||||||
Self {
|
Self {
|
||||||
gradient_format: GradientInterpolation {
|
gradient_format: GradientInterpolation {
|
||||||
color_space: GradientColorSpace::Srgb,
|
color_space: GradientColorSpace::Srgb,
|
||||||
@@ -29,19 +29,20 @@ impl TestCase for GradientSrgbAlpha {
|
|||||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
let (a, b) = (size.w / 6, size.h / 3);
|
let (a, b) = (size.w / 6, size.h / 3);
|
||||||
let size = (size.w - a * 2, size.h - b * 2);
|
let size = (size.w - a * 2, size.h - b * 2);
|
||||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||||
|
|
||||||
[BorderRenderElement::new(
|
[BorderRenderElement::new(
|
||||||
area.size,
|
area.size,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
self.gradient_format,
|
self.gradient_format,
|
||||||
Color::new_unpremul(1., 0., 0., 1.),
|
Color::new_unpremul(1., 0., 0., 1.),
|
||||||
Color::new_unpremul(0., 1., 0., 0.),
|
Color::new_unpremul(0., 1., 0., 0.),
|
||||||
0.,
|
0.,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
0.,
|
0.,
|
||||||
CornerRadius::default(),
|
CornerRadius::default(),
|
||||||
1.,
|
1.,
|
||||||
|
1.,
|
||||||
)
|
)
|
||||||
.with_location(area.loc)]
|
.with_location(area.loc)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ use niri_config::{
|
|||||||
};
|
};
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use super::TestCase;
|
use super::{Args, TestCase};
|
||||||
|
|
||||||
pub struct GradientSrgbLinear {
|
pub struct GradientSrgbLinear {
|
||||||
gradient_format: GradientInterpolation,
|
gradient_format: GradientInterpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientSrgbLinear {
|
impl GradientSrgbLinear {
|
||||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
pub fn new(_args: Args) -> Self {
|
||||||
Self {
|
Self {
|
||||||
gradient_format: GradientInterpolation {
|
gradient_format: GradientInterpolation {
|
||||||
color_space: GradientColorSpace::SrgbLinear,
|
color_space: GradientColorSpace::SrgbLinear,
|
||||||
@@ -31,19 +31,20 @@ impl TestCase for GradientSrgbLinear {
|
|||||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
let (a, b) = (size.w / 6, size.h / 3);
|
let (a, b) = (size.w / 6, size.h / 3);
|
||||||
let size = (size.w - a * 2, size.h - b * 2);
|
let size = (size.w - a * 2, size.h - b * 2);
|
||||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||||
|
|
||||||
[BorderRenderElement::new(
|
[BorderRenderElement::new(
|
||||||
area.size,
|
area.size,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
self.gradient_format,
|
self.gradient_format,
|
||||||
Color::new_unpremul(1., 0., 0., 1.),
|
Color::new_unpremul(1., 0., 0., 1.),
|
||||||
Color::new_unpremul(0., 1., 0., 1.),
|
Color::new_unpremul(0., 1., 0., 1.),
|
||||||
0.,
|
0.,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
0.,
|
0.,
|
||||||
CornerRadius::default(),
|
CornerRadius::default(),
|
||||||
1.,
|
1.,
|
||||||
|
1.,
|
||||||
)
|
)
|
||||||
.with_location(area.loc)]
|
.with_location(area.loc)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ use niri::render_helpers::border::BorderRenderElement;
|
|||||||
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
|
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use super::TestCase;
|
use super::{Args, TestCase};
|
||||||
|
|
||||||
pub struct GradientSrgbLinearAlpha {
|
pub struct GradientSrgbLinearAlpha {
|
||||||
gradient_format: GradientInterpolation,
|
gradient_format: GradientInterpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientSrgbLinearAlpha {
|
impl GradientSrgbLinearAlpha {
|
||||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
pub fn new(_args: Args) -> Self {
|
||||||
Self {
|
Self {
|
||||||
gradient_format: GradientInterpolation {
|
gradient_format: GradientInterpolation {
|
||||||
color_space: GradientColorSpace::SrgbLinear,
|
color_space: GradientColorSpace::SrgbLinear,
|
||||||
@@ -29,19 +29,20 @@ impl TestCase for GradientSrgbLinearAlpha {
|
|||||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
let (a, b) = (size.w / 6, size.h / 3);
|
let (a, b) = (size.w / 6, size.h / 3);
|
||||||
let size = (size.w - a * 2, size.h - b * 2);
|
let size = (size.w - a * 2, size.h - b * 2);
|
||||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
let area = Rectangle::new(Point::from((a, b)), Size::from(size)).to_f64();
|
||||||
|
|
||||||
[BorderRenderElement::new(
|
[BorderRenderElement::new(
|
||||||
area.size,
|
area.size,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
self.gradient_format,
|
self.gradient_format,
|
||||||
Color::new_unpremul(1., 0., 0., 1.),
|
Color::new_unpremul(1., 0., 0., 1.),
|
||||||
Color::new_unpremul(0., 1., 0., 0.),
|
Color::new_unpremul(0., 1., 0., 0.),
|
||||||
0.,
|
0.,
|
||||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
Rectangle::from_size(area.size),
|
||||||
0.,
|
0.,
|
||||||
CornerRadius::default(),
|
CornerRadius::default(),
|
||||||
1.,
|
1.,
|
||||||
|
1.,
|
||||||
)
|
)
|
||||||
.with_location(area.loc)]
|
.with_location(area.loc)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use niri::layout::workspace::ColumnWidth;
|
use niri::animation::Clock;
|
||||||
use niri::layout::{LayoutElement as _, Options};
|
use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options};
|
||||||
use niri::render_helpers::RenderTarget;
|
use niri::render_helpers::RenderTarget;
|
||||||
use niri::utils::get_monotonic_time;
|
use niri_config::{Color, FloatOrInt, OutputName, PresetSize};
|
||||||
use niri_config::{Color, FloatOrInt, OutputName};
|
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::desktop::layer_map_for_output;
|
use smithay::desktop::layer_map_for_output;
|
||||||
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
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;
|
use crate::test_window::TestWindow;
|
||||||
|
|
||||||
type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
|
type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
|
||||||
@@ -20,13 +19,16 @@ type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
|
|||||||
pub struct Layout {
|
pub struct Layout {
|
||||||
output: Output,
|
output: Output,
|
||||||
windows: Vec<TestWindow>,
|
windows: Vec<TestWindow>,
|
||||||
|
clock: Clock,
|
||||||
layout: niri::layout::Layout<TestWindow>,
|
layout: niri::layout::Layout<TestWindow>,
|
||||||
start_time: Duration,
|
start_time: Duration,
|
||||||
steps: HashMap<Duration, DynStepFn>,
|
steps: HashMap<Duration, DynStepFn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Layout {
|
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(
|
let output = Output::new(
|
||||||
String::new(),
|
String::new(),
|
||||||
PhysicalProperties {
|
PhysicalProperties {
|
||||||
@@ -58,46 +60,51 @@ impl Layout {
|
|||||||
width: FloatOrInt(4.),
|
width: FloatOrInt(4.),
|
||||||
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
|
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
|
||||||
inactive_color: Color::from_rgba8_unpremul(50, 50, 50, 255),
|
inactive_color: Color::from_rgba8_unpremul(50, 50, 50, 255),
|
||||||
|
urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255),
|
||||||
active_gradient: None,
|
active_gradient: None,
|
||||||
inactive_gradient: None,
|
inactive_gradient: None,
|
||||||
|
urgent_gradient: None,
|
||||||
},
|
},
|
||||||
..Default::default()
|
..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());
|
layout.add_output(output.clone());
|
||||||
|
|
||||||
|
let start_time = clock.now_unadjusted();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
output,
|
output,
|
||||||
windows: Vec::new(),
|
windows: Vec::new(),
|
||||||
|
clock,
|
||||||
layout,
|
layout,
|
||||||
start_time: get_monotonic_time(),
|
start_time,
|
||||||
steps: HashMap::new(),
|
steps: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_in_between(size: Size<i32, Logical>) -> Self {
|
pub fn open_in_between(args: Args) -> Self {
|
||||||
let mut rv = Self::new(size);
|
let mut rv = Self::new(args);
|
||||||
|
|
||||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
|
||||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
|
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.3)));
|
||||||
rv.layout.activate_window(&0);
|
rv.layout.activate_window(&0);
|
||||||
|
|
||||||
rv.add_step(500, |l| {
|
rv.add_step(500, |l| {
|
||||||
let win = TestWindow::freeform(2);
|
let win = TestWindow::freeform(2);
|
||||||
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
|
l.add_window(win.clone(), Some(PresetSize::Proportion(0.3)));
|
||||||
l.layout.start_open_animation_for_window(win.id());
|
l.layout.start_open_animation_for_window(win.id());
|
||||||
});
|
});
|
||||||
|
|
||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_multiple_quickly(size: Size<i32, Logical>) -> Self {
|
pub fn open_multiple_quickly(args: Args) -> Self {
|
||||||
let mut rv = Self::new(size);
|
let mut rv = Self::new(args);
|
||||||
|
|
||||||
for delay in [100, 200, 300] {
|
for delay in [100, 200, 300] {
|
||||||
rv.add_step(delay, move |l| {
|
rv.add_step(delay, move |l| {
|
||||||
let win = TestWindow::freeform(delay as usize);
|
let win = TestWindow::freeform(delay as usize);
|
||||||
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
|
l.add_window(win.clone(), Some(PresetSize::Proportion(0.3)));
|
||||||
l.layout.start_open_animation_for_window(win.id());
|
l.layout.start_open_animation_for_window(win.id());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -105,13 +112,13 @@ impl Layout {
|
|||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_multiple_quickly_big(size: Size<i32, Logical>) -> Self {
|
pub fn open_multiple_quickly_big(args: Args) -> Self {
|
||||||
let mut rv = Self::new(size);
|
let mut rv = Self::new(args);
|
||||||
|
|
||||||
for delay in [100, 200, 300] {
|
for delay in [100, 200, 300] {
|
||||||
rv.add_step(delay, move |l| {
|
rv.add_step(delay, move |l| {
|
||||||
let win = TestWindow::freeform(delay as usize);
|
let win = TestWindow::freeform(delay as usize);
|
||||||
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.5)));
|
l.add_window(win.clone(), Some(PresetSize::Proportion(0.5)));
|
||||||
l.layout.start_open_animation_for_window(win.id());
|
l.layout.start_open_animation_for_window(win.id());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -119,44 +126,59 @@ impl Layout {
|
|||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_to_the_left(size: Size<i32, Logical>) -> Self {
|
pub fn open_to_the_left(args: Args) -> Self {
|
||||||
let mut rv = Self::new(size);
|
let mut rv = Self::new(args);
|
||||||
|
|
||||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
|
||||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
|
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.3)));
|
||||||
|
|
||||||
rv.add_step(500, |l| {
|
rv.add_step(500, |l| {
|
||||||
let win = TestWindow::freeform(2);
|
let win = TestWindow::freeform(2);
|
||||||
let right_of = l.windows[0].clone();
|
let right_of = l.windows[0].clone();
|
||||||
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.3)));
|
l.add_window_right_of(&right_of, win.clone(), Some(PresetSize::Proportion(0.3)));
|
||||||
l.layout.start_open_animation_for_window(win.id());
|
l.layout.start_open_animation_for_window(win.id());
|
||||||
});
|
});
|
||||||
|
|
||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_to_the_left_big(size: Size<i32, Logical>) -> Self {
|
pub fn open_to_the_left_big(args: Args) -> Self {
|
||||||
let mut rv = Self::new(size);
|
let mut rv = Self::new(args);
|
||||||
|
|
||||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
|
||||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.8)));
|
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.8)));
|
||||||
|
|
||||||
rv.add_step(500, |l| {
|
rv.add_step(500, |l| {
|
||||||
let win = TestWindow::freeform(2);
|
let win = TestWindow::freeform(2);
|
||||||
let right_of = l.windows[0].clone();
|
let right_of = l.windows[0].clone();
|
||||||
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.5)));
|
l.add_window_right_of(&right_of, win.clone(), Some(PresetSize::Proportion(0.5)));
|
||||||
l.layout.start_open_animation_for_window(win.id());
|
l.layout.start_open_animation_for_window(win.id());
|
||||||
});
|
});
|
||||||
|
|
||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) {
|
fn add_window(&mut self, mut window: TestWindow, width: Option<PresetSize>) {
|
||||||
let ws = self.layout.active_workspace().unwrap();
|
let ws = self.layout.active_workspace().unwrap();
|
||||||
window.request_size(ws.new_window_size(width, window.rules()), false, None);
|
let min_size = window.min_size();
|
||||||
|
let max_size = window.max_size();
|
||||||
|
window.request_size(
|
||||||
|
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
);
|
||||||
window.communicate();
|
window.communicate();
|
||||||
|
|
||||||
self.layout.add_window(window.clone(), width, false);
|
self.layout.add_window(
|
||||||
|
window.clone(),
|
||||||
|
AddWindowTarget::Auto,
|
||||||
|
width,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
ActivateWindow::default(),
|
||||||
|
);
|
||||||
self.windows.push(window);
|
self.windows.push(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,14 +186,28 @@ impl Layout {
|
|||||||
&mut self,
|
&mut self,
|
||||||
right_of: &TestWindow,
|
right_of: &TestWindow,
|
||||||
mut window: TestWindow,
|
mut window: TestWindow,
|
||||||
width: Option<ColumnWidth>,
|
width: Option<PresetSize>,
|
||||||
) {
|
) {
|
||||||
let ws = self.layout.active_workspace().unwrap();
|
let ws = self.layout.active_workspace().unwrap();
|
||||||
window.request_size(ws.new_window_size(width, window.rules()), false, None);
|
let min_size = window.min_size();
|
||||||
|
let max_size = window.max_size();
|
||||||
|
window.request_size(
|
||||||
|
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
);
|
||||||
window.communicate();
|
window.communicate();
|
||||||
|
|
||||||
self.layout
|
self.layout.add_window(
|
||||||
.add_window_right_of(right_of.id(), window.clone(), width, false);
|
window.clone(),
|
||||||
|
AddWindowTarget::NextTo(right_of.id()),
|
||||||
|
width,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
ActivateWindow::default(),
|
||||||
|
);
|
||||||
self.windows.push(window);
|
self.windows.push(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,22 +237,25 @@ impl TestCase for Layout {
|
|||||||
self.layout.are_animations_ongoing(Some(&self.output)) || !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
|
let run = self
|
||||||
.steps
|
.steps
|
||||||
.keys()
|
.keys()
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|delay| self.start_time + *delay <= current_time)
|
.filter(|delay| self.start_time + *delay <= now_unadjusted)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
for key in &run {
|
for delay in &run {
|
||||||
let f = self.steps.remove(key).unwrap();
|
let now = self.start_time + *delay;
|
||||||
|
self.clock.set_unadjusted(now);
|
||||||
|
self.layout.advance_animations();
|
||||||
|
|
||||||
|
let f = self.steps.remove(delay).unwrap();
|
||||||
f(self);
|
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(
|
fn render(
|
||||||
@@ -228,7 +267,8 @@ impl TestCase for Layout {
|
|||||||
self.layout
|
self.layout
|
||||||
.monitor_for_output(&self.output)
|
.monitor_for_output(&self.output)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.render_elements(renderer, RenderTarget::Output)
|
.render_elements(renderer, RenderTarget::Output, true)
|
||||||
|
.flat_map(|(_, iter)| iter)
|
||||||
.map(|elem| Box::new(elem) as _)
|
.map(|elem| Box::new(elem) as _)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use niri::animation::Clock;
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Physical, Size};
|
use smithay::utils::{Logical, Physical, Size};
|
||||||
|
|
||||||
pub mod gradient_angle;
|
pub mod gradient_angle;
|
||||||
pub mod gradient_area;
|
pub mod gradient_area;
|
||||||
@@ -21,6 +22,11 @@ pub mod layout;
|
|||||||
pub mod tile;
|
pub mod tile;
|
||||||
pub mod window;
|
pub mod window;
|
||||||
|
|
||||||
|
pub struct Args {
|
||||||
|
pub size: Size<i32, Logical>,
|
||||||
|
pub clock: Clock,
|
||||||
|
}
|
||||||
|
|
||||||
pub trait TestCase {
|
pub trait TestCase {
|
||||||
fn resize(&mut self, _width: i32, _height: i32) {}
|
fn resize(&mut self, _width: i32, _height: i32) {}
|
||||||
fn are_animations_ongoing(&self) -> bool {
|
fn are_animations_ongoing(&self) -> bool {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ use niri::render_helpers::RenderTarget;
|
|||||||
use niri_config::{Color, FloatOrInt};
|
use niri_config::{Color, FloatOrInt};
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
|
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use super::TestCase;
|
use super::{Args, TestCase};
|
||||||
use crate::test_window::TestWindow;
|
use crate::test_window::TestWindow;
|
||||||
|
|
||||||
pub struct Tile {
|
pub struct Tile {
|
||||||
@@ -17,53 +17,46 @@ pub struct Tile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Tile {
|
impl Tile {
|
||||||
pub fn freeform(size: Size<i32, Logical>) -> Self {
|
pub fn freeform(args: Args) -> Self {
|
||||||
let window = TestWindow::freeform(0);
|
let window = TestWindow::freeform(0);
|
||||||
let mut rv = Self::with_window(window);
|
Self::with_window(args, window)
|
||||||
rv.tile.request_tile_size(size.to_f64(), false, None);
|
|
||||||
rv.window.communicate();
|
|
||||||
rv
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
|
pub fn fixed_size(args: Args) -> Self {
|
||||||
let window = TestWindow::fixed_size(0);
|
let window = TestWindow::fixed_size(0);
|
||||||
let mut rv = Self::with_window(window);
|
Self::with_window(args, window)
|
||||||
rv.tile.request_tile_size(size.to_f64(), false, None);
|
|
||||||
rv.window.communicate();
|
|
||||||
rv
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
let window = TestWindow::fixed_size(0);
|
||||||
window.set_csd_shadow_width(64);
|
window.set_csd_shadow_width(64);
|
||||||
let mut rv = Self::with_window(window);
|
Self::with_window(args, window)
|
||||||
rv.tile.request_tile_size(size.to_f64(), false, None);
|
|
||||||
rv.window.communicate();
|
|
||||||
rv
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn freeform_open(size: Size<i32, Logical>) -> Self {
|
pub fn freeform_open(args: Args) -> Self {
|
||||||
let mut rv = Self::freeform(size);
|
let mut rv = Self::freeform(args);
|
||||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||||
rv.tile.start_open_animation();
|
rv.tile.start_open_animation();
|
||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fixed_size_open(size: Size<i32, Logical>) -> Self {
|
pub fn fixed_size_open(args: Args) -> Self {
|
||||||
let mut rv = Self::fixed_size(size);
|
let mut rv = Self::fixed_size(args);
|
||||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||||
rv.tile.start_open_animation();
|
rv.tile.start_open_animation();
|
||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fixed_size_with_csd_shadow_open(size: Size<i32, Logical>) -> Self {
|
pub fn fixed_size_with_csd_shadow_open(args: Args) -> Self {
|
||||||
let mut rv = Self::fixed_size_with_csd_shadow(size);
|
let mut rv = Self::fixed_size_with_csd_shadow(args);
|
||||||
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||||
rv.tile.start_open_animation();
|
rv.tile.start_open_animation();
|
||||||
rv
|
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 {
|
let options = Options {
|
||||||
focus_ring: niri_config::FocusRing {
|
focus_ring: niri_config::FocusRing {
|
||||||
off: true,
|
off: true,
|
||||||
@@ -77,15 +70,28 @@ impl Tile {
|
|||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let tile = niri::layout::tile::Tile::new(window.clone(), 1., 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 }
|
Self { window, tile }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestCase for Tile {
|
impl TestCase for Tile {
|
||||||
fn resize(&mut self, width: i32, height: i32) {
|
fn resize(&mut self, width: i32, height: i32) {
|
||||||
|
let size = Size::from((width, height)).to_f64();
|
||||||
self.tile
|
self.tile
|
||||||
.request_tile_size(Size::from((width, height)).to_f64(), false, None);
|
.update_config(size, 1., self.tile.options().clone());
|
||||||
|
self.tile.request_tile_size(size, false, None);
|
||||||
self.window.communicate();
|
self.window.communicate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,8 +99,8 @@ impl TestCase for Tile {
|
|||||||
self.tile.are_animations_ongoing()
|
self.tile.are_animations_ongoing()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn advance_animations(&mut self, current_time: Duration) {
|
fn advance_animations(&mut self, _current_time: Duration) {
|
||||||
self.tile.advance_animations(current_time);
|
self.tile.advance_animations();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(
|
fn render(
|
||||||
@@ -106,18 +112,12 @@ impl TestCase for Tile {
|
|||||||
let tile_size = self.tile.tile_size().to_physical(1.);
|
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.);
|
let location = Point::from((size.w - tile_size.w, size.h - tile_size.h)).downscale(2.);
|
||||||
|
|
||||||
self.tile.update(
|
self.tile.update_render_elements(
|
||||||
true,
|
true,
|
||||||
Rectangle::from_loc_and_size((-location.x, -location.y), size.to_logical(1.)),
|
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
|
||||||
);
|
);
|
||||||
self.tile
|
self.tile
|
||||||
.render(
|
.render(renderer, location, true, RenderTarget::Output)
|
||||||
renderer,
|
|
||||||
location,
|
|
||||||
Scale::from(1.),
|
|
||||||
true,
|
|
||||||
RenderTarget::Output,
|
|
||||||
)
|
|
||||||
.map(|elem| Box::new(elem) as _)
|
.map(|elem| Box::new(elem) as _)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ use niri::layout::LayoutElement;
|
|||||||
use niri::render_helpers::RenderTarget;
|
use niri::render_helpers::RenderTarget;
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
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;
|
use crate::test_window::TestWindow;
|
||||||
|
|
||||||
pub struct Window {
|
pub struct Window {
|
||||||
@@ -12,24 +12,24 @@ pub struct Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
pub fn freeform(size: Size<i32, Logical>) -> Self {
|
pub fn freeform(args: Args) -> Self {
|
||||||
let mut window = TestWindow::freeform(0);
|
let mut window = TestWindow::freeform(0);
|
||||||
window.request_size(size, false, None);
|
window.request_size(args.size, false, false, None);
|
||||||
window.communicate();
|
window.communicate();
|
||||||
Self { window }
|
Self { window }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
|
pub fn fixed_size(args: Args) -> Self {
|
||||||
let mut window = TestWindow::fixed_size(0);
|
let mut window = TestWindow::fixed_size(0);
|
||||||
window.request_size(size, false, None);
|
window.request_size(args.size, false, false, None);
|
||||||
window.communicate();
|
window.communicate();
|
||||||
Self { window }
|
Self { window }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
|
pub fn fixed_size_with_csd_shadow(args: Args) -> Self {
|
||||||
let mut window = TestWindow::fixed_size(0);
|
let mut window = TestWindow::fixed_size(0);
|
||||||
window.set_csd_shadow_width(64);
|
window.set_csd_shadow_width(64);
|
||||||
window.request_size(size, false, None);
|
window.request_size(args.size, false, false, None);
|
||||||
window.communicate();
|
window.communicate();
|
||||||
Self { window }
|
Self { window }
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ impl Window {
|
|||||||
impl TestCase for Window {
|
impl TestCase for Window {
|
||||||
fn resize(&mut self, width: i32, height: i32) {
|
fn resize(&mut self, width: i32, height: i32) {
|
||||||
self.window
|
self.window
|
||||||
.request_size(Size::from((width, height)), false, None);
|
.request_size(Size::from((width, height)), false, false, None);
|
||||||
self.window.communicate();
|
self.window.communicate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,11 @@
|
|||||||
extern crate tracing;
|
extern crate tracing;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
|
|
||||||
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
|
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
|
||||||
use gtk::prelude::{
|
use cases::Args;
|
||||||
AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt,
|
use gtk::prelude::{ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt};
|
||||||
};
|
|
||||||
use gtk::{gdk, gio, glib};
|
use gtk::{gdk, gio, glib};
|
||||||
use niri::animation::ANIMATION_SLOWDOWN;
|
|
||||||
use smithay::utils::{Logical, Size};
|
|
||||||
use smithay_view::SmithayView;
|
use smithay_view::SmithayView;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
@@ -66,24 +62,23 @@ fn on_startup(_app: &adw::Application) {
|
|||||||
|
|
||||||
fn build_ui(app: &adw::Application) {
|
fn build_ui(app: &adw::Application) {
|
||||||
let stack = gtk::Stack::new();
|
let stack = gtk::Stack::new();
|
||||||
|
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
|
||||||
|
|
||||||
struct S {
|
struct S {
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
|
anim_adjustment: gtk::Adjustment,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl S {
|
impl S {
|
||||||
fn add<T: TestCase + 'static>(
|
fn add<T: TestCase + 'static>(&self, make: impl Fn(Args) -> T + 'static, title: &str) {
|
||||||
&self,
|
let view = SmithayView::new(make, &self.anim_adjustment);
|
||||||
make: impl Fn(Size<i32, Logical>) -> T + 'static,
|
|
||||||
title: &str,
|
|
||||||
) {
|
|
||||||
let view = SmithayView::new(make);
|
|
||||||
self.stack.add_titled(&view, None, title);
|
self.stack.add_titled(&view, None, title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let s = S {
|
let s = S {
|
||||||
stack: stack.clone(),
|
stack: stack.clone(),
|
||||||
|
anim_adjustment: anim_adjustment.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
s.add(Window::freeform, "Freeform Window");
|
s.add(Window::freeform, "Freeform Window");
|
||||||
@@ -137,9 +132,6 @@ fn build_ui(app: &adw::Application) {
|
|||||||
|
|
||||||
let content_headerbar = adw::HeaderBar::new();
|
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));
|
let anim_scale = gtk::Scale::new(gtk::Orientation::Horizontal, Some(&anim_adjustment));
|
||||||
anim_scale.set_hexpand(true);
|
anim_scale.set_hexpand(true);
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,44 @@
|
|||||||
use gtk::glib;
|
use gtk::glib;
|
||||||
|
use gtk::prelude::*;
|
||||||
use gtk::subclass::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 {
|
mod imp {
|
||||||
use std::cell::{Cell, OnceCell, RefCell};
|
use std::cell::{Cell, OnceCell, RefCell};
|
||||||
use std::ptr::null;
|
use std::ptr::null;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{ensure, Context};
|
use anyhow::{ensure, Context};
|
||||||
use gtk::gdk;
|
use gtk::gdk;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
use niri::animation::Clock;
|
||||||
use niri::render_helpers::{resources, shaders};
|
use niri::render_helpers::{resources, shaders};
|
||||||
use niri::utils::get_monotonic_time;
|
|
||||||
use smithay::backend::egl::ffi::egl;
|
use smithay::backend::egl::ffi::egl;
|
||||||
use smithay::backend::egl::EGLContext;
|
use smithay::backend::egl::EGLContext;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||||
use smithay::backend::renderer::{Color32F, Frame, Renderer, Unbind};
|
use smithay::backend::renderer::{Bind, Color32F, Frame, Offscreen, Renderer};
|
||||||
|
use smithay::reexports::gbm::Format as Fourcc;
|
||||||
use smithay::utils::{Physical, Rectangle, Scale, Transform};
|
use smithay::utils::{Physical, Rectangle, Scale, Transform};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
type DynMakeTestCase = Box<dyn Fn(Size<i32, Logical>) -> Box<dyn TestCase>>;
|
type DynMakeTestCase = Box<dyn Fn(Args) -> Box<dyn TestCase>>;
|
||||||
|
|
||||||
|
struct RendererData {
|
||||||
|
renderer: GlesRenderer,
|
||||||
|
dummy_texture: GlesTexture,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct SmithayView {
|
pub struct SmithayView {
|
||||||
gl_area: gtk::GLArea,
|
gl_area: gtk::GLArea,
|
||||||
size: Cell<(i32, i32)>,
|
size: Cell<(i32, i32)>,
|
||||||
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
|
renderer: RefCell<Option<Result<RendererData, ()>>>,
|
||||||
pub make_test_case: OnceCell<DynMakeTestCase>,
|
pub make_test_case: OnceCell<DynMakeTestCase>,
|
||||||
test_case: RefCell<Option<Box<dyn TestCase>>>,
|
test_case: RefCell<Option<Box<dyn TestCase>>>,
|
||||||
|
pub clock: RefCell<Clock>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
@@ -122,30 +131,71 @@ mod imp {
|
|||||||
let Ok(renderer) = renderer else {
|
let Ok(renderer) = renderer else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
let RendererData {
|
||||||
|
renderer,
|
||||||
|
dummy_texture,
|
||||||
|
} = renderer;
|
||||||
|
|
||||||
let size = self.size.get();
|
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.
|
// Create the test case if missing.
|
||||||
let mut case = self.test_case.borrow_mut();
|
let mut case = self.test_case.borrow_mut();
|
||||||
let case = case.get_or_insert_with(|| {
|
let case = case.get_or_insert_with(|| {
|
||||||
let make = self.make_test_case.get().unwrap();
|
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 {
|
// Fetch GtkGLArea's framebuffer binding.
|
||||||
with_framebuffer_save_restore(renderer, |renderer| {
|
let mut framebuffer = 0;
|
||||||
case.render(renderer, Size::from(size))
|
renderer
|
||||||
|
.with_context(|gl| unsafe {
|
||||||
|
gl.GetIntegerv(
|
||||||
|
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
|
||||||
|
&mut framebuffer,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
}?;
|
.context("error running closure in GL context")?;
|
||||||
|
ensure!(framebuffer != 0, "error getting the framebuffer");
|
||||||
|
|
||||||
|
// This call will already change the framebuffer binding (offscreen elements will bind
|
||||||
|
// intermediate textures during rendering).
|
||||||
|
let elements = case.render(renderer, Size::from(size));
|
||||||
|
|
||||||
|
// HACK: there's currently no way to "just" render into an externally bound framebuffer
|
||||||
|
// (like we have in this case). The render() call requires a valid target. So what
|
||||||
|
// we'll do is use a dummy texture as a target, then swap the framebuffer binding right
|
||||||
|
// before rendering.
|
||||||
|
let mut dummy_target = renderer
|
||||||
|
.bind(dummy_texture)
|
||||||
|
.context("error binding dummy texture")?;
|
||||||
|
|
||||||
let mut frame = renderer
|
let mut frame = renderer
|
||||||
.render(rect.size, Transform::Normal)
|
.render(&mut dummy_target, rect.size, Transform::Normal)
|
||||||
.context("error creating frame")?;
|
.context("error creating frame")?;
|
||||||
|
|
||||||
|
// Now that render() bound the dummy texture, change the binding underneath it back to
|
||||||
|
// GtkGLArea's framebuffer, to render there instead.
|
||||||
|
frame
|
||||||
|
.with_context(|gl| unsafe {
|
||||||
|
gl.BindFramebuffer(
|
||||||
|
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
|
||||||
|
framebuffer as u32,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.context("error running closure in GL context")?;
|
||||||
|
|
||||||
frame
|
frame
|
||||||
.clear(Color32F::from([0.3, 0.3, 0.3, 1.]), &[rect])
|
.clear(Color32F::from([0.3, 0.3, 0.3, 1.]), &[rect])
|
||||||
.context("error clearing")?;
|
.context("error clearing")?;
|
||||||
@@ -166,7 +216,7 @@ mod imp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn create_renderer() -> anyhow::Result<GlesRenderer> {
|
unsafe fn create_renderer() -> anyhow::Result<RendererData> {
|
||||||
smithay::backend::egl::ffi::make_sure_egl_is_loaded()
|
smithay::backend::egl::ffi::make_sure_egl_is_loaded()
|
||||||
.context("error loading EGL symbols in Smithay")?;
|
.context("error loading EGL symbols in Smithay")?;
|
||||||
|
|
||||||
@@ -189,40 +239,17 @@ mod imp {
|
|||||||
|
|
||||||
let mut renderer = GlesRenderer::new(egl_context).context("error creating GlesRenderer")?;
|
let mut renderer = GlesRenderer::new(egl_context).context("error creating GlesRenderer")?;
|
||||||
|
|
||||||
|
let dummy_texture = renderer
|
||||||
|
.create_buffer(Fourcc::Abgr8888, Size::from((1, 1)))
|
||||||
|
.context("error creating dummy texture")?;
|
||||||
|
|
||||||
resources::init(&mut renderer);
|
resources::init(&mut renderer);
|
||||||
shaders::init(&mut renderer);
|
shaders::init(&mut renderer);
|
||||||
|
|
||||||
Ok(renderer)
|
Ok(RendererData {
|
||||||
}
|
renderer,
|
||||||
|
dummy_texture,
|
||||||
unsafe fn with_framebuffer_save_restore<T>(
|
})
|
||||||
renderer: &mut GlesRenderer,
|
|
||||||
f: impl FnOnce(&mut GlesRenderer) -> T,
|
|
||||||
) -> anyhow::Result<T> {
|
|
||||||
let mut framebuffer = 0;
|
|
||||||
renderer
|
|
||||||
.with_context(|gl| unsafe {
|
|
||||||
gl.GetIntegerv(
|
|
||||||
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
|
|
||||||
&mut framebuffer,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.context("error running closure in GL context")?;
|
|
||||||
ensure!(framebuffer != 0, "error getting the framebuffer");
|
|
||||||
|
|
||||||
let rv = f(renderer);
|
|
||||||
|
|
||||||
renderer.unbind().context("error unbinding")?;
|
|
||||||
renderer
|
|
||||||
.with_context(|gl| unsafe {
|
|
||||||
gl.BindFramebuffer(
|
|
||||||
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
|
|
||||||
framebuffer as u32,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.context("error running closure in GL context")?;
|
|
||||||
|
|
||||||
Ok(rv)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,14 +260,32 @@ glib::wrapper! {
|
|||||||
|
|
||||||
impl SmithayView {
|
impl SmithayView {
|
||||||
pub fn new<T: TestCase + 'static>(
|
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 {
|
) -> Self {
|
||||||
let obj: Self = glib::Object::builder().build();
|
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 make_test_case = Box::new(make) as _;
|
||||||
let _ = obj.imp().make_test_case.set(make_test_case);
|
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
|
obj
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ use niri::layout::{
|
|||||||
ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement,
|
ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement,
|
||||||
LayoutElementRenderSnapshot,
|
LayoutElementRenderSnapshot,
|
||||||
};
|
};
|
||||||
|
use niri::render_helpers::offscreen::OffscreenData;
|
||||||
use niri::render_helpers::renderer::NiriRenderer;
|
use niri::render_helpers::renderer::NiriRenderer;
|
||||||
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||||
use niri::render_helpers::{RenderTarget, SplitElements};
|
use niri::render_helpers::{RenderTarget, SplitElements};
|
||||||
use niri::utils::transaction::Transaction;
|
use niri::utils::transaction::Transaction;
|
||||||
use niri::window::ResolvedWindowRules;
|
use niri::window::ResolvedWindowRules;
|
||||||
use smithay::backend::renderer::element::{Id, Kind};
|
use smithay::backend::renderer::element::Kind;
|
||||||
use smithay::output::{self, Output};
|
use smithay::output::{self, Output};
|
||||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform};
|
use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform};
|
||||||
@@ -181,15 +182,12 @@ impl LayoutElement for TestWindow {
|
|||||||
fn request_size(
|
fn request_size(
|
||||||
&mut self,
|
&mut self,
|
||||||
size: Size<i32, Logical>,
|
size: Size<i32, Logical>,
|
||||||
|
is_fullscreen: bool,
|
||||||
_animate: bool,
|
_animate: bool,
|
||||||
_transaction: Option<Transaction>,
|
_transaction: Option<Transaction>,
|
||||||
) {
|
) {
|
||||||
self.inner.borrow_mut().requested_size = Some(size);
|
self.inner.borrow_mut().requested_size = Some(size);
|
||||||
self.inner.borrow_mut().pending_fullscreen = false;
|
self.inner.borrow_mut().pending_fullscreen = is_fullscreen;
|
||||||
}
|
|
||||||
|
|
||||||
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
|
|
||||||
self.inner.borrow_mut().pending_fullscreen = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn min_size(&self) -> Size<i32, Logical> {
|
fn min_size(&self) -> Size<i32, Logical> {
|
||||||
@@ -214,14 +212,20 @@ impl LayoutElement for TestWindow {
|
|||||||
|
|
||||||
fn output_leave(&self, _output: &Output) {}
|
fn output_leave(&self, _output: &Output) {}
|
||||||
|
|
||||||
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
|
fn set_offscreen_data(&self, _data: Option<OffscreenData>) {}
|
||||||
|
|
||||||
fn set_activated(&mut self, _active: bool) {}
|
fn set_activated(&mut self, _active: bool) {}
|
||||||
|
|
||||||
fn set_active_in_column(&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 set_bounds(&self, _bounds: Size<i32, Logical>) {}
|
||||||
|
|
||||||
|
fn is_ignoring_opacity_window_rule(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn configure_intent(&self) -> ConfigureIntent {
|
fn configure_intent(&self) -> ConfigureIntent {
|
||||||
ConfigureIntent::CanSend
|
ConfigureIntent::CanSend
|
||||||
}
|
}
|
||||||
@@ -240,6 +244,10 @@ impl LayoutElement for TestWindow {
|
|||||||
self.inner.borrow().requested_size
|
self.inner.borrow().requested_size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_child_of(&self, _parent: &Self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn refresh(&self) {}
|
fn refresh(&self) {}
|
||||||
|
|
||||||
fn rules(&self) -> &ResolvedWindowRules {
|
fn rules(&self) -> &ResolvedWindowRules {
|
||||||
@@ -259,9 +267,13 @@ impl LayoutElement for TestWindow {
|
|||||||
|
|
||||||
fn cancel_interactive_resize(&mut self) {}
|
fn cancel_interactive_resize(&mut self) {}
|
||||||
|
|
||||||
fn update_interactive_resize(&mut self, _serial: Serial) {}
|
fn on_commit(&mut self, _serial: Serial) {}
|
||||||
|
|
||||||
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
|
fn interactive_resize_data(&self) -> Option<InteractiveResizeData> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_urgent(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-7
@@ -33,7 +33,6 @@ Summary: Scrollable-tiling Wayland compositor
|
|||||||
|
|
||||||
SourceLicense: GPL-3.0-or-later
|
SourceLicense: GPL-3.0-or-later
|
||||||
|
|
||||||
# (MIT OR Apache-2.0) AND BSD-3-Clause
|
|
||||||
# 0BSD OR MIT OR Apache-2.0
|
# 0BSD OR MIT OR Apache-2.0
|
||||||
# Apache-2.0
|
# Apache-2.0
|
||||||
# Apache-2.0 OR BSL-1.0
|
# Apache-2.0 OR BSL-1.0
|
||||||
@@ -41,18 +40,21 @@ SourceLicense: GPL-3.0-or-later
|
|||||||
# Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT
|
# Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT
|
||||||
# BSD-2-Clause
|
# BSD-2-Clause
|
||||||
# BSD-2-Clause OR Apache-2.0 OR MIT
|
# BSD-2-Clause OR Apache-2.0 OR MIT
|
||||||
# BSD-3-Clause
|
|
||||||
# BSD-3-Clause OR MIT OR Apache-2.0
|
# BSD-3-Clause OR MIT OR Apache-2.0
|
||||||
# GPL-3.0-or-later
|
# GPL-3.0-or-later
|
||||||
# ISC
|
# ISC
|
||||||
# MIT
|
# MIT
|
||||||
|
# MIT AND (MIT OR Apache-2.0)
|
||||||
# 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 Apache-2.0 OR Zlib
|
||||||
# MIT OR Zlib OR Apache-2.0
|
# MIT OR Zlib OR Apache-2.0
|
||||||
# MPL-2.0
|
# MPL-2.0
|
||||||
|
# Unicode-3.0
|
||||||
# Unlicense OR MIT
|
# Unlicense OR MIT
|
||||||
# Zlib OR Apache-2.0 OR MIT
|
# 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 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 (Unlicense OR MIT) AND (Zlib OR Apache-2.0 OR MIT)
|
License: (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT AND (MIT OR Apache-2.0)) AND (MIT OR Apache-2.0) AND ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unicode-3.0) AND (Unlicense OR MIT) AND (Zlib OR Apache-2.0 OR MIT)
|
||||||
# LICENSE.dependencies contains a full license breakdown
|
# LICENSE.dependencies contains a full license breakdown
|
||||||
|
|
||||||
URL: https://github.com/YaLTeR/niri
|
URL: https://github.com/YaLTeR/niri
|
||||||
@@ -87,6 +89,7 @@ Recommends: gnome-keyring
|
|||||||
Recommends: alacritty
|
Recommends: alacritty
|
||||||
Recommends: fuzzel
|
Recommends: fuzzel
|
||||||
Recommends: swaylock
|
Recommends: swaylock
|
||||||
|
Recommends: waybar
|
||||||
# Suggested utilities
|
# Suggested utilities
|
||||||
Recommends: swaybg
|
Recommends: swaybg
|
||||||
Recommends: mako
|
Recommends: mako
|
||||||
@@ -101,10 +104,6 @@ Opening a new window never causes existing windows to resize.
|
|||||||
%prep
|
%prep
|
||||||
{{{ git_dir_setup_macro }}}
|
{{{ git_dir_setup_macro }}}
|
||||||
|
|
||||||
# Make the version log message look nicer: since we're building not from niri's git repository,
|
|
||||||
# the git version macro will show its fallback string.
|
|
||||||
sed -i 's/"unknown commit"/"%{version}"/' src/utils/mod.rs
|
|
||||||
|
|
||||||
%cargo_prep -N
|
%cargo_prep -N
|
||||||
|
|
||||||
# We're doing an online build.
|
# We're doing an online build.
|
||||||
@@ -113,6 +112,9 @@ sed -i 's/^offline = true$//' .cargo/config.toml
|
|||||||
# Final step in leaving alone our debug settings.
|
# Final step in leaving alone our debug settings.
|
||||||
sed -i 's/.*please-remove-me$//' .cargo/config.toml
|
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
|
%build
|
||||||
%cargo_build
|
%cargo_build
|
||||||
|
|
||||||
|
|||||||
+101
-11
@@ -1,7 +1,7 @@
|
|||||||
// This config is in the KDL format: https://kdl.dev
|
// This config is in the KDL format: https://kdl.dev
|
||||||
// "/-" comments out the following node.
|
// "/-" comments out the following node.
|
||||||
// Check the wiki for a full description of the configuration:
|
// Check the wiki for a full description of the configuration:
|
||||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Overview
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Introduction
|
||||||
|
|
||||||
// Input device configuration.
|
// Input device configuration.
|
||||||
// Find the full list of options on the wiki:
|
// Find the full list of options on the wiki:
|
||||||
@@ -16,6 +16,9 @@ input {
|
|||||||
// layout "us,ru"
|
// layout "us,ru"
|
||||||
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
|
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable numlock on startup, omitting this setting disables it.
|
||||||
|
numlock
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next sections include libinput settings.
|
// Next sections include libinput settings.
|
||||||
@@ -25,6 +28,8 @@ input {
|
|||||||
tap
|
tap
|
||||||
// dwt
|
// dwt
|
||||||
// dwtp
|
// dwtp
|
||||||
|
// drag false
|
||||||
|
// drag-lock
|
||||||
natural-scroll
|
natural-scroll
|
||||||
// accel-speed 0.2
|
// accel-speed 0.2
|
||||||
// accel-profile "flat"
|
// accel-profile "flat"
|
||||||
@@ -187,10 +192,50 @@ layout {
|
|||||||
active-color "#ffc87f"
|
active-color "#ffc87f"
|
||||||
inactive-color "#505050"
|
inactive-color "#505050"
|
||||||
|
|
||||||
|
// Color of the border around windows that request your attention.
|
||||||
|
urgent-color "#9b0000"
|
||||||
|
|
||||||
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
|
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
|
||||||
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// You can enable drop shadows for windows.
|
||||||
|
shadow {
|
||||||
|
// Uncomment the next line to enable shadows.
|
||||||
|
// on
|
||||||
|
|
||||||
|
// By default, the shadow draws only around its window, and not behind it.
|
||||||
|
// Uncomment this setting to make the shadow draw behind its window.
|
||||||
|
//
|
||||||
|
// Note that niri has no way of knowing about the CSD window corner
|
||||||
|
// radius. It has to assume that windows have square corners, leading to
|
||||||
|
// shadow artifacts inside the CSD rounded corners. This setting fixes
|
||||||
|
// those artifacts.
|
||||||
|
//
|
||||||
|
// However, instead you may want to set prefer-no-csd and/or
|
||||||
|
// geometry-corner-radius. Then, niri will know the corner radius and
|
||||||
|
// draw the shadow correctly, without having to draw it behind the
|
||||||
|
// window. These will also remove client-side shadows if the window
|
||||||
|
// draws any.
|
||||||
|
//
|
||||||
|
// draw-behind-window true
|
||||||
|
|
||||||
|
// You can change how shadows look. The values below are in logical
|
||||||
|
// pixels and match the CSS box-shadow properties.
|
||||||
|
|
||||||
|
// Softness controls the shadow blur radius.
|
||||||
|
softness 30
|
||||||
|
|
||||||
|
// Spread expands the shadow.
|
||||||
|
spread 5
|
||||||
|
|
||||||
|
// Offset moves the shadow relative to the window.
|
||||||
|
offset x=0 y=5
|
||||||
|
|
||||||
|
// You can also change the shadow color and opacity.
|
||||||
|
color "#0007"
|
||||||
|
}
|
||||||
|
|
||||||
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
|
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
|
||||||
// You can think of them as a kind of outer gaps. They are set in logical pixels.
|
// You can think of them as a kind of outer gaps. They are set in logical pixels.
|
||||||
// Left and right struts will cause the next window to the side to always be visible.
|
// Left and right struts will cause the next window to the side to always be visible.
|
||||||
@@ -208,7 +253,9 @@ layout {
|
|||||||
// Note that running niri as a session supports xdg-desktop-autostart,
|
// Note that running niri as a session supports xdg-desktop-autostart,
|
||||||
// which may be more convenient to use.
|
// which may be more convenient to use.
|
||||||
// See the binds section below for more spawn examples.
|
// See the binds section below for more spawn examples.
|
||||||
// spawn-at-startup "alacritty" "-e" "fish"
|
|
||||||
|
// This line starts waybar, a commonly used bar for Wayland compositors.
|
||||||
|
spawn-at-startup "waybar"
|
||||||
|
|
||||||
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
|
// 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.
|
// If the client will specifically ask for CSD, the request will be honored.
|
||||||
@@ -250,6 +297,15 @@ window-rule {
|
|||||||
default-column-width {}
|
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.
|
// Example: block out two password managers from screen capture.
|
||||||
// (This example rule is commented out with a "/-" in front.)
|
// (This example rule is commented out with a "/-" in front.)
|
||||||
/-window-rule {
|
/-window-rule {
|
||||||
@@ -285,9 +341,9 @@ binds {
|
|||||||
Mod+Shift+Slash { show-hotkey-overlay; }
|
Mod+Shift+Slash { show-hotkey-overlay; }
|
||||||
|
|
||||||
// Suggested binds for running programs: terminal, app launcher, screen locker.
|
// Suggested binds for running programs: terminal, app launcher, screen locker.
|
||||||
Mod+T { spawn "alacritty"; }
|
Mod+T hotkey-overlay-title="Open a Terminal: alacritty" { spawn "alacritty"; }
|
||||||
Mod+D { spawn "fuzzel"; }
|
Mod+D hotkey-overlay-title="Run an Application: fuzzel" { spawn "fuzzel"; }
|
||||||
Super+Alt+L { spawn "swaylock"; }
|
Super+Alt+L hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; }
|
||||||
|
|
||||||
// You can also use a shell. Do this if you need pipes, multiple commands, etc.
|
// 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.
|
// Note: the entire command goes as a single argument in the end.
|
||||||
@@ -300,6 +356,11 @@ binds {
|
|||||||
XF86AudioMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
|
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"; }
|
XF86AudioMicMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }
|
||||||
|
|
||||||
|
// Open/close the Overview: a zoomed-out view of workspaces and windows.
|
||||||
|
// You can also move the mouse into the top-left hot corner,
|
||||||
|
// or do a four-finger swipe up on a touchpad.
|
||||||
|
Mod+O repeat=false { toggle-overview; }
|
||||||
|
|
||||||
Mod+Q { close-window; }
|
Mod+Q { close-window; }
|
||||||
|
|
||||||
Mod+Left { focus-column-left; }
|
Mod+Left { focus-column-left; }
|
||||||
@@ -441,22 +502,32 @@ binds {
|
|||||||
// Switches focus between the current and the previous workspace.
|
// Switches focus between the current and the previous workspace.
|
||||||
// Mod+Tab { focus-workspace-previous; }
|
// Mod+Tab { focus-workspace-previous; }
|
||||||
|
|
||||||
// Consume one window from the right into the focused column.
|
// The following binds move the focused window in and out of a column.
|
||||||
Mod+Comma { consume-window-into-column; }
|
// If the window is alone, they will consume it into the nearby column to the side.
|
||||||
// Expel one window from the focused column to the right.
|
// If the window is already in a column, they will expel it out.
|
||||||
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+BracketLeft { consume-or-expel-window-left; }
|
||||||
Mod+BracketRight { consume-or-expel-window-right; }
|
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; }
|
||||||
|
|
||||||
Mod+R { switch-preset-column-width; }
|
Mod+R { switch-preset-column-width; }
|
||||||
Mod+Shift+R { switch-preset-window-height; }
|
Mod+Shift+R { switch-preset-window-height; }
|
||||||
Mod+Ctrl+R { reset-window-height; }
|
Mod+Ctrl+R { reset-window-height; }
|
||||||
Mod+F { maximize-column; }
|
Mod+F { maximize-column; }
|
||||||
Mod+Shift+F { fullscreen-window; }
|
Mod+Shift+F { fullscreen-window; }
|
||||||
|
|
||||||
|
// Expand the focused column to space not taken up by other fully visible columns.
|
||||||
|
// Makes the column "fill the rest of the space".
|
||||||
|
Mod+Ctrl+F { expand-column-to-available-width; }
|
||||||
|
|
||||||
Mod+C { center-column; }
|
Mod+C { center-column; }
|
||||||
|
|
||||||
|
// Center all fully visible columns on screen.
|
||||||
|
Mod+Ctrl+C { center-visible-columns; }
|
||||||
|
|
||||||
// Finer width adjustments.
|
// Finer width adjustments.
|
||||||
// This command can also:
|
// This command can also:
|
||||||
// * set width in pixels: "1000"
|
// * set width in pixels: "1000"
|
||||||
@@ -472,6 +543,15 @@ binds {
|
|||||||
Mod+Shift+Minus { set-window-height "-10%"; }
|
Mod+Shift+Minus { set-window-height "-10%"; }
|
||||||
Mod+Shift+Equal { set-window-height "+10%"; }
|
Mod+Shift+Equal { set-window-height "+10%"; }
|
||||||
|
|
||||||
|
// Move the focused window between the floating and the tiling layout.
|
||||||
|
Mod+V { toggle-window-floating; }
|
||||||
|
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
|
||||||
|
|
||||||
|
// Toggle tabbed column display mode.
|
||||||
|
// Windows in this column will appear as vertical tabs,
|
||||||
|
// rather than stacked on top of each other.
|
||||||
|
Mod+W { toggle-column-tabbed-display; }
|
||||||
|
|
||||||
// Actions to switch layouts.
|
// Actions to switch layouts.
|
||||||
// Note: if you uncomment these, make sure you do NOT have
|
// Note: if you uncomment these, make sure you do NOT have
|
||||||
// a matching layout switch hotkey configured in xkb options above.
|
// a matching layout switch hotkey configured in xkb options above.
|
||||||
@@ -484,6 +564,16 @@ binds {
|
|||||||
Ctrl+Print { screenshot-screen; }
|
Ctrl+Print { screenshot-screen; }
|
||||||
Alt+Print { screenshot-window; }
|
Alt+Print { screenshot-window; }
|
||||||
|
|
||||||
|
// Applications such as remote-desktop clients and software KVM switches may
|
||||||
|
// request that niri stops processing the keyboard shortcuts defined here
|
||||||
|
// so they may, for example, forward the key presses as-is to a remote machine.
|
||||||
|
// It's a good idea to bind an escape hatch to toggle the inhibitor,
|
||||||
|
// so a buggy application can't hold your session hostage.
|
||||||
|
//
|
||||||
|
// The allow-inhibiting=false property can be applied to other binds as well,
|
||||||
|
// which ensures niri always processes them, even when an inhibitor is active.
|
||||||
|
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
|
||||||
|
|
||||||
// The quit action will show a confirmation dialog to avoid accidental exits.
|
// The quit action will show a confirmation dialog to avoid accidental exits.
|
||||||
Mod+Shift+E { quit; }
|
Mod+Shift+E { quit; }
|
||||||
Ctrl+Alt+Delete { quit; }
|
Ctrl+Alt+Delete { quit; }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
[preferred]
|
[preferred]
|
||||||
default=gnome;gtk;
|
default=gnome;gtk;
|
||||||
org.freedesktop.impl.portal.Access=gtk;
|
org.freedesktop.impl.portal.Access=gtk;
|
||||||
|
org.freedesktop.impl.portal.Notification=gtk;
|
||||||
org.freedesktop.impl.portal.Secret=gnome-keyring;
|
org.freedesktop.impl.portal.Secret=gnome-keyring;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ if [ -n "$SHELL" ] &&
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Try to detect the service manager that is being used
|
# Try to detect the service manager that is being used
|
||||||
if hash systemctl &> /dev/null; then
|
if hash systemctl >/dev/null 2>&1; then
|
||||||
# Make sure there's no already running session.
|
# Make sure there's no already running session.
|
||||||
if systemctl --user -q is-active niri.service; then
|
if systemctl --user -q is-active niri.service; then
|
||||||
echo 'A niri session is already running.'
|
echo 'A niri session is already running.'
|
||||||
@@ -41,15 +41,15 @@ if hash systemctl &> /dev/null; then
|
|||||||
|
|
||||||
# Unset environment that we've set.
|
# Unset environment that we've set.
|
||||||
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
|
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
|
||||||
elif hash dinitctl &> /dev/null; then
|
elif hash dinitctl >/dev/null 2>&1; then
|
||||||
# Check that the user dinit daemon is running
|
# Check that the user dinit daemon is running
|
||||||
if ! pgrep -u $(id -u) dinit &> /dev/null; then
|
if ! pgrep -u "$(id -u)" dinit >/dev/null 2>&1; then
|
||||||
echo "dinit user daemon is not running."
|
echo "dinit user daemon is not running."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Make sure there's no already running session.
|
# Make sure there's no already running session.
|
||||||
if dinitctl --user is-started niri &> /dev/null; then
|
if dinitctl --user is-started niri >/dev/null 2>&1; then
|
||||||
echo 'A niri session is already running.'
|
echo 'A niri session is already running.'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
+82
-103
@@ -2,14 +2,12 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use keyframe::functions::{EaseOutCubic, EaseOutQuad};
|
use keyframe::functions::{EaseOutCubic, EaseOutQuad};
|
||||||
use keyframe::EasingFunction;
|
use keyframe::EasingFunction;
|
||||||
use portable_atomic::{AtomicF64, Ordering};
|
|
||||||
|
|
||||||
use crate::utils::get_monotonic_time;
|
|
||||||
|
|
||||||
mod spring;
|
mod spring;
|
||||||
pub use spring::{Spring, SpringParams};
|
pub use spring::{Spring, SpringParams};
|
||||||
|
|
||||||
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
|
mod clock;
|
||||||
|
pub use clock::Clock;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Animation {
|
pub struct Animation {
|
||||||
@@ -23,7 +21,7 @@ pub struct Animation {
|
|||||||
/// Best effort; not always exactly precise.
|
/// Best effort; not always exactly precise.
|
||||||
clamped_duration: Duration,
|
clamped_duration: Duration,
|
||||||
start_time: Duration,
|
start_time: Duration,
|
||||||
current_time: Duration,
|
clock: Clock,
|
||||||
kind: Kind,
|
kind: Kind,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,11 +46,17 @@ pub enum Curve {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Animation {
|
impl Animation {
|
||||||
pub fn new(from: f64, to: f64, initial_velocity: f64, config: niri_config::Animation) -> Self {
|
pub fn new(
|
||||||
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
|
clock: Clock,
|
||||||
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
from: f64,
|
||||||
|
to: f64,
|
||||||
|
initial_velocity: f64,
|
||||||
|
config: niri_config::Animation,
|
||||||
|
) -> Self {
|
||||||
|
// Scale the velocity by rate to keep the touchpad gestures feeling right.
|
||||||
|
let initial_velocity = initial_velocity / clock.rate().max(0.001);
|
||||||
|
|
||||||
let mut rv = Self::ease(from, to, initial_velocity, 0, Curve::EaseOutCubic);
|
let mut rv = Self::ease(clock, from, to, initial_velocity, 0, Curve::EaseOutCubic);
|
||||||
if config.off {
|
if config.off {
|
||||||
rv.is_off = true;
|
rv.is_off = true;
|
||||||
return rv;
|
return rv;
|
||||||
@@ -71,7 +75,6 @@ impl Animation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let start_time = self.start_time;
|
let start_time = self.start_time;
|
||||||
let current_time = self.current_time;
|
|
||||||
|
|
||||||
match config.kind {
|
match config.kind {
|
||||||
niri_config::AnimationKind::Spring(p) => {
|
niri_config::AnimationKind::Spring(p) => {
|
||||||
@@ -83,10 +86,11 @@ impl Animation {
|
|||||||
initial_velocity: self.initial_velocity,
|
initial_velocity: self.initial_velocity,
|
||||||
params,
|
params,
|
||||||
};
|
};
|
||||||
*self = Self::spring(spring);
|
*self = Self::spring(self.clock.clone(), spring);
|
||||||
}
|
}
|
||||||
niri_config::AnimationKind::Easing(p) => {
|
niri_config::AnimationKind::Easing(p) => {
|
||||||
*self = Self::ease(
|
*self = Self::ease(
|
||||||
|
self.clock.clone(),
|
||||||
self.from,
|
self.from,
|
||||||
self.to,
|
self.to,
|
||||||
self.initial_velocity,
|
self.initial_velocity,
|
||||||
@@ -97,7 +101,6 @@ impl Animation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.start_time = start_time;
|
self.start_time = start_time;
|
||||||
self.current_time = current_time;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restarts the animation using the previous config.
|
/// Restarts the animation using the previous config.
|
||||||
@@ -106,11 +109,12 @@ impl Animation {
|
|||||||
return self.clone();
|
return self.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
|
// Scale the velocity by rate to keep the touchpad gestures feeling right.
|
||||||
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
let initial_velocity = initial_velocity / self.clock.rate().max(0.001);
|
||||||
|
|
||||||
match self.kind {
|
match self.kind {
|
||||||
Kind::Easing { curve } => Self::ease(
|
Kind::Easing { curve } => Self::ease(
|
||||||
|
self.clock.clone(),
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
initial_velocity,
|
initial_velocity,
|
||||||
@@ -119,28 +123,37 @@ impl Animation {
|
|||||||
),
|
),
|
||||||
Kind::Spring(spring) => {
|
Kind::Spring(spring) => {
|
||||||
let spring = Spring {
|
let spring = Spring {
|
||||||
from: self.from,
|
from,
|
||||||
to: self.to,
|
to,
|
||||||
initial_velocity: self.initial_velocity,
|
initial_velocity: self.initial_velocity,
|
||||||
params: spring.params,
|
params: spring.params,
|
||||||
};
|
};
|
||||||
Self::spring(spring)
|
Self::spring(self.clock.clone(), spring)
|
||||||
}
|
}
|
||||||
Kind::Deceleration {
|
Kind::Deceleration {
|
||||||
initial_velocity,
|
initial_velocity,
|
||||||
deceleration_rate,
|
deceleration_rate,
|
||||||
} => {
|
} => {
|
||||||
let threshold = 0.001; // FIXME
|
let threshold = 0.001; // FIXME
|
||||||
Self::decelerate(from, initial_velocity, deceleration_rate, threshold)
|
Self::decelerate(
|
||||||
|
self.clock.clone(),
|
||||||
|
from,
|
||||||
|
initial_velocity,
|
||||||
|
deceleration_rate,
|
||||||
|
threshold,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ease(from: f64, to: f64, initial_velocity: f64, duration_ms: u64, curve: Curve) -> Self {
|
pub fn ease(
|
||||||
// FIXME: ideally we shouldn't use current time here because animations started within the
|
clock: Clock,
|
||||||
// same frame cycle should have the same start time to be synchronized.
|
from: f64,
|
||||||
let now = get_monotonic_time();
|
to: f64,
|
||||||
|
initial_velocity: f64,
|
||||||
|
duration_ms: u64,
|
||||||
|
curve: Curve,
|
||||||
|
) -> Self {
|
||||||
let duration = Duration::from_millis(duration_ms);
|
let duration = Duration::from_millis(duration_ms);
|
||||||
let kind = Kind::Easing { curve };
|
let kind = Kind::Easing { curve };
|
||||||
|
|
||||||
@@ -152,19 +165,15 @@ impl Animation {
|
|||||||
duration,
|
duration,
|
||||||
// Our current curves never overshoot.
|
// Our current curves never overshoot.
|
||||||
clamped_duration: duration,
|
clamped_duration: duration,
|
||||||
start_time: now,
|
start_time: clock.now(),
|
||||||
current_time: now,
|
clock,
|
||||||
kind,
|
kind,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spring(spring: Spring) -> Self {
|
pub fn spring(clock: Clock, spring: Spring) -> Self {
|
||||||
let _span = tracy_client::span!("Animation::spring");
|
let _span = tracy_client::span!("Animation::spring");
|
||||||
|
|
||||||
// FIXME: ideally we shouldn't use current time here because animations started within the
|
|
||||||
// same frame cycle should have the same start time to be synchronized.
|
|
||||||
let now = get_monotonic_time();
|
|
||||||
|
|
||||||
let duration = spring.duration();
|
let duration = spring.duration();
|
||||||
let clamped_duration = spring.clamped_duration().unwrap_or(duration);
|
let clamped_duration = spring.clamped_duration().unwrap_or(duration);
|
||||||
let kind = Kind::Spring(spring);
|
let kind = Kind::Spring(spring);
|
||||||
@@ -176,22 +185,19 @@ impl Animation {
|
|||||||
is_off: false,
|
is_off: false,
|
||||||
duration,
|
duration,
|
||||||
clamped_duration,
|
clamped_duration,
|
||||||
start_time: now,
|
start_time: clock.now(),
|
||||||
current_time: now,
|
clock,
|
||||||
kind,
|
kind,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decelerate(
|
pub fn decelerate(
|
||||||
|
clock: Clock,
|
||||||
from: f64,
|
from: f64,
|
||||||
initial_velocity: f64,
|
initial_velocity: f64,
|
||||||
deceleration_rate: f64,
|
deceleration_rate: f64,
|
||||||
threshold: f64,
|
threshold: f64,
|
||||||
) -> Self {
|
) -> 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. {
|
let duration_s = if initial_velocity == 0. {
|
||||||
0.
|
0.
|
||||||
} else {
|
} else {
|
||||||
@@ -214,85 +220,43 @@ impl Animation {
|
|||||||
is_off: false,
|
is_off: false,
|
||||||
duration,
|
duration,
|
||||||
clamped_duration: duration,
|
clamped_duration: duration,
|
||||||
start_time: now,
|
start_time: clock.now(),
|
||||||
current_time: now,
|
clock,
|
||||||
kind,
|
kind,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_current_time(&mut self, time: Duration) {
|
|
||||||
if self.duration.is_zero() {
|
|
||||||
self.current_time = time;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let end_time = self.start_time + self.duration;
|
|
||||||
if end_time <= self.current_time {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
|
||||||
if slowdown <= f64::EPSILON {
|
|
||||||
// Zero slowdown will cause the animation to end right away.
|
|
||||||
self.current_time = end_time;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can't change current_time (since the incoming time values are always real-time), so
|
|
||||||
// apply the slowdown by shifting the start time to compensate.
|
|
||||||
if self.current_time <= time {
|
|
||||||
let delta = time - self.current_time;
|
|
||||||
|
|
||||||
let max_delta = end_time - self.current_time;
|
|
||||||
let min_slowdown = delta.as_secs_f64() / max_delta.as_secs_f64();
|
|
||||||
if slowdown <= min_slowdown {
|
|
||||||
// Our slowdown value will cause the animation to end right away.
|
|
||||||
self.current_time = end_time;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let adjusted_delta = delta.div_f64(slowdown);
|
|
||||||
if adjusted_delta >= delta {
|
|
||||||
self.start_time -= adjusted_delta - delta;
|
|
||||||
} else {
|
|
||||||
self.start_time += delta - adjusted_delta;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let delta = self.current_time - time;
|
|
||||||
|
|
||||||
let min_slowdown = delta.as_secs_f64() / self.current_time.as_secs_f64();
|
|
||||||
if slowdown <= min_slowdown {
|
|
||||||
// Current time was about to jump to before the animation had started; let's just
|
|
||||||
// cancel the animation in this case.
|
|
||||||
self.current_time = end_time;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let adjusted_delta = delta.div_f64(slowdown);
|
|
||||||
if adjusted_delta >= delta {
|
|
||||||
self.start_time += adjusted_delta - delta;
|
|
||||||
} else {
|
|
||||||
self.start_time -= delta - adjusted_delta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.current_time = time;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_done(&self) -> bool {
|
pub fn is_done(&self) -> bool {
|
||||||
self.current_time >= self.start_time + self.duration
|
if self.clock.should_complete_instantly() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.clock.now() >= self.start_time + self.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_clamped_done(&self) -> bool {
|
pub fn is_clamped_done(&self) -> bool {
|
||||||
self.current_time >= self.start_time + self.clamped_duration
|
if self.clock.should_complete_instantly() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.clock.now() >= self.start_time + self.clamped_duration
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn value(&self) -> f64 {
|
pub fn value_at(&self, at: Duration) -> f64 {
|
||||||
if self.is_done() {
|
if at <= self.start_time {
|
||||||
|
// Return from when at == start_time so that when the animations are off, the behavior
|
||||||
|
// within a single event loop cycle (i.e. no time had passed since the start of an
|
||||||
|
// animation) matches the behavior when the animations are on.
|
||||||
|
return self.from;
|
||||||
|
} else if self.start_time + self.duration <= at {
|
||||||
return self.to;
|
return self.to;
|
||||||
}
|
}
|
||||||
|
|
||||||
let passed = self.current_time.saturating_sub(self.start_time);
|
if self.clock.should_complete_instantly() {
|
||||||
|
return self.to;
|
||||||
|
}
|
||||||
|
|
||||||
|
let passed = at.saturating_sub(self.start_time);
|
||||||
|
|
||||||
match self.kind {
|
match self.kind {
|
||||||
Kind::Easing { curve } => {
|
Kind::Easing { curve } => {
|
||||||
@@ -325,6 +289,10 @@ impl Animation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn value(&self) -> f64 {
|
||||||
|
self.value_at(self.clock.now())
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a value that stops at the target value after first reaching it.
|
/// Returns a value that stops at the target value after first reaching it.
|
||||||
///
|
///
|
||||||
/// Best effort; not always exactly precise.
|
/// Best effort; not always exactly precise.
|
||||||
@@ -340,11 +308,22 @@ impl Animation {
|
|||||||
self.to
|
self.to
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn from(&self) -> f64 {
|
pub fn from(&self) -> f64 {
|
||||||
self.from
|
self.from
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn start_time(&self) -> Duration {
|
||||||
|
self.start_time
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_time(&self) -> Duration {
|
||||||
|
self.start_time + self.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn duration(&self) -> Duration {
|
||||||
|
self.duration
|
||||||
|
}
|
||||||
|
|
||||||
pub fn offset(&mut self, offset: f64) {
|
pub fn offset(&mut self, offset: f64) {
|
||||||
self.from += offset;
|
self.from += offset;
|
||||||
self.to += offset;
|
self.to += offset;
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ impl Spring {
|
|||||||
return Duration::MAX;
|
return Duration::MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self.to - self.from).abs() <= f64::EPSILON {
|
||||||
|
return Duration::ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
let omega0 = (self.params.stiffness / self.params.mass).sqrt();
|
let omega0 = (self.params.stiffness / self.params.mass).sqrt();
|
||||||
|
|
||||||
// As first ansatz for the overdamped solution,
|
// As first ansatz for the overdamped solution,
|
||||||
@@ -90,6 +94,12 @@ impl Spring {
|
|||||||
|
|
||||||
x1 = (self.to - y0 + m * x0) / m;
|
x1 = (self.to - y0 + m * x0) / m;
|
||||||
y1 = self.oscillate(x1);
|
y1 = self.oscillate(x1);
|
||||||
|
|
||||||
|
// Overdamped springs have some numerical stability issues...
|
||||||
|
if !y1.is_finite() {
|
||||||
|
return Duration::from_secs_f64(x0);
|
||||||
|
}
|
||||||
|
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,3 +176,34 @@ impl Spring {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overdamped_spring_equal_from_to_nan() {
|
||||||
|
let spring = Spring {
|
||||||
|
from: 0.,
|
||||||
|
to: 0.,
|
||||||
|
initial_velocity: 0.,
|
||||||
|
params: SpringParams::new(1.15, 850., 0.0001),
|
||||||
|
};
|
||||||
|
let _ = spring.duration();
|
||||||
|
let _ = spring.clamped_duration();
|
||||||
|
let _ = spring.value_at(Duration::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overdamped_spring_duration_panic() {
|
||||||
|
let spring = Spring {
|
||||||
|
from: 0.,
|
||||||
|
to: 1.,
|
||||||
|
initial_velocity: 0.,
|
||||||
|
params: SpringParams::new(6., 1200., 0.0001),
|
||||||
|
};
|
||||||
|
let _ = spring.duration();
|
||||||
|
let _ = spring.clamped_duration();
|
||||||
|
let _ = spring.value_at(Duration::ZERO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
//! Headless backend for tests.
|
||||||
|
//!
|
||||||
|
//! This can eventually grow into a more complete backend if needed, but for now it's missing some
|
||||||
|
//! crucial parts like rendering.
|
||||||
|
|
||||||
|
use std::mem;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use niri_config::OutputName;
|
||||||
|
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||||
|
use smithay::backend::renderer::element::RenderElementStates;
|
||||||
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
|
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||||
|
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||||
|
use smithay::utils::Size;
|
||||||
|
use smithay::wayland::presentation::Refresh;
|
||||||
|
|
||||||
|
use super::{IpcOutputMap, OutputId, RenderResult};
|
||||||
|
use crate::niri::{Niri, RedrawState};
|
||||||
|
use crate::utils::{get_monotonic_time, logical_output};
|
||||||
|
|
||||||
|
pub struct Headless {
|
||||||
|
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Headless {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
ipc_outputs: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(&mut self, _niri: &mut Niri) {}
|
||||||
|
|
||||||
|
pub fn add_output(&mut self, niri: &mut Niri, n: u8, size: (u16, u16)) {
|
||||||
|
let connector = format!("headless-{n}");
|
||||||
|
let make = "niri".to_string();
|
||||||
|
let model = "headless".to_string();
|
||||||
|
let serial = n.to_string();
|
||||||
|
|
||||||
|
let output = Output::new(
|
||||||
|
connector.clone(),
|
||||||
|
PhysicalProperties {
|
||||||
|
size: (0, 0).into(),
|
||||||
|
subpixel: Subpixel::Unknown,
|
||||||
|
make: make.clone(),
|
||||||
|
model: model.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mode = Mode {
|
||||||
|
size: Size::from((i32::from(size.0), i32::from(size.1))),
|
||||||
|
refresh: 60_000,
|
||||||
|
};
|
||||||
|
output.change_current_state(Some(mode), None, None, None);
|
||||||
|
output.set_preferred(mode);
|
||||||
|
|
||||||
|
output.user_data().insert_if_missing(|| OutputName {
|
||||||
|
connector,
|
||||||
|
make: Some(make),
|
||||||
|
model: Some(model),
|
||||||
|
serial: Some(serial),
|
||||||
|
});
|
||||||
|
|
||||||
|
let physical_properties = output.physical_properties();
|
||||||
|
self.ipc_outputs.lock().unwrap().insert(
|
||||||
|
OutputId::next(),
|
||||||
|
niri_ipc::Output {
|
||||||
|
name: output.name(),
|
||||||
|
make: physical_properties.make,
|
||||||
|
model: physical_properties.model,
|
||||||
|
serial: None,
|
||||||
|
physical_size: None,
|
||||||
|
modes: vec![niri_ipc::Mode {
|
||||||
|
width: size.0,
|
||||||
|
height: size.1,
|
||||||
|
refresh_rate: 60_000,
|
||||||
|
is_preferred: true,
|
||||||
|
}],
|
||||||
|
current_mode: Some(0),
|
||||||
|
vrr_supported: false,
|
||||||
|
vrr_enabled: false,
|
||||||
|
logical: Some(logical_output(&output)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
niri.add_output(output, None, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seat_name(&self) -> String {
|
||||||
|
"headless".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_primary_renderer<T>(
|
||||||
|
&mut self,
|
||||||
|
_f: impl FnOnce(&mut GlesRenderer) -> T,
|
||||||
|
) -> Option<T> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&mut self, niri: &mut Niri, output: &Output) -> RenderResult {
|
||||||
|
let states = RenderElementStates::default();
|
||||||
|
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &states);
|
||||||
|
presentation_feedbacks.presented::<_, smithay::utils::Monotonic>(
|
||||||
|
get_monotonic_time(),
|
||||||
|
Refresh::Unknown,
|
||||||
|
0,
|
||||||
|
wp_presentation_feedback::Kind::empty(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let output_state = niri.output_state.get_mut(output).unwrap();
|
||||||
|
match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
|
||||||
|
RedrawState::Idle => unreachable!(),
|
||||||
|
RedrawState::Queued => (),
|
||||||
|
RedrawState::WaitingForVBlank { .. } => unreachable!(),
|
||||||
|
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
|
||||||
|
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1);
|
||||||
|
|
||||||
|
// FIXME: request redraw on unfinished animations remain
|
||||||
|
|
||||||
|
RenderResult::Submitted
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_dmabuf(&mut self, _dmabuf: &Dmabuf) -> bool {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
|
||||||
|
self.ipc_outputs.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Headless {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
-11
@@ -2,12 +2,12 @@ use std::collections::HashMap;
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use niri_config::{Config, ModKey};
|
||||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::output::Output;
|
use smithay::output::Output;
|
||||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
|
|
||||||
use crate::input::CompositorMod;
|
|
||||||
use crate::niri::Niri;
|
use crate::niri::Niri;
|
||||||
use crate::utils::id::IdCounter;
|
use crate::utils::id::IdCounter;
|
||||||
|
|
||||||
@@ -17,9 +17,14 @@ pub use tty::Tty;
|
|||||||
pub mod winit;
|
pub mod winit;
|
||||||
pub use winit::Winit;
|
pub use winit::Winit;
|
||||||
|
|
||||||
|
pub mod headless;
|
||||||
|
pub use headless::Headless;
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum Backend {
|
pub enum Backend {
|
||||||
Tty(Tty),
|
Tty(Tty),
|
||||||
Winit(Winit),
|
Winit(Winit),
|
||||||
|
Headless(Headless),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
@@ -54,6 +59,7 @@ impl Backend {
|
|||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.init(niri),
|
Backend::Tty(tty) => tty.init(niri),
|
||||||
Backend::Winit(winit) => winit.init(niri),
|
Backend::Winit(winit) => winit.init(niri),
|
||||||
|
Backend::Headless(headless) => headless.init(niri),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +67,7 @@ impl Backend {
|
|||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.seat_name(),
|
Backend::Tty(tty) => tty.seat_name(),
|
||||||
Backend::Winit(winit) => winit.seat_name(),
|
Backend::Winit(winit) => winit.seat_name(),
|
||||||
|
Backend::Headless(headless) => headless.seat_name(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +78,7 @@ impl Backend {
|
|||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.with_primary_renderer(f),
|
Backend::Tty(tty) => tty.with_primary_renderer(f),
|
||||||
Backend::Winit(winit) => winit.with_primary_renderer(f),
|
Backend::Winit(winit) => winit.with_primary_renderer(f),
|
||||||
|
Backend::Headless(headless) => headless.with_primary_renderer(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,13 +91,20 @@ impl Backend {
|
|||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.render(niri, output, target_presentation_time),
|
Backend::Tty(tty) => tty.render(niri, output, target_presentation_time),
|
||||||
Backend::Winit(winit) => winit.render(niri, output),
|
Backend::Winit(winit) => winit.render(niri, output),
|
||||||
|
Backend::Headless(headless) => headless.render(niri, output),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mod_key(&self) -> CompositorMod {
|
pub fn mod_key(&self, config: &Config) -> ModKey {
|
||||||
match self {
|
match self {
|
||||||
Backend::Tty(_) => CompositorMod::Super,
|
Backend::Winit(_) => config.input.mod_key_nested.unwrap_or({
|
||||||
Backend::Winit(_) => CompositorMod::Alt,
|
if let Some(ModKey::Alt) = config.input.mod_key {
|
||||||
|
ModKey::Super
|
||||||
|
} else {
|
||||||
|
ModKey::Alt
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Backend::Tty(_) | Backend::Headless(_) => config.input.mod_key.unwrap_or(ModKey::Super),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +112,7 @@ impl Backend {
|
|||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.change_vt(vt),
|
Backend::Tty(tty) => tty.change_vt(vt),
|
||||||
Backend::Winit(_) => (),
|
Backend::Winit(_) => (),
|
||||||
|
Backend::Headless(_) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +120,7 @@ impl Backend {
|
|||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.suspend(),
|
Backend::Tty(tty) => tty.suspend(),
|
||||||
Backend::Winit(_) => (),
|
Backend::Winit(_) => (),
|
||||||
|
Backend::Headless(_) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +128,7 @@ impl Backend {
|
|||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.toggle_debug_tint(),
|
Backend::Tty(tty) => tty.toggle_debug_tint(),
|
||||||
Backend::Winit(winit) => winit.toggle_debug_tint(),
|
Backend::Winit(winit) => winit.toggle_debug_tint(),
|
||||||
|
Backend::Headless(_) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +136,7 @@ impl Backend {
|
|||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
|
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
|
||||||
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
|
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
|
||||||
|
Backend::Headless(headless) => headless.import_dmabuf(dmabuf),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +144,7 @@ impl Backend {
|
|||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.early_import(surface),
|
Backend::Tty(tty) => tty.early_import(surface),
|
||||||
Backend::Winit(_) => (),
|
Backend::Winit(_) => (),
|
||||||
|
Backend::Headless(_) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +152,7 @@ impl Backend {
|
|||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.ipc_outputs(),
|
Backend::Tty(tty) => tty.ipc_outputs(),
|
||||||
Backend::Winit(winit) => winit.ipc_outputs(),
|
Backend::Winit(winit) => winit.ipc_outputs(),
|
||||||
|
Backend::Headless(headless) => headless.ipc_outputs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +164,7 @@ impl Backend {
|
|||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.primary_gbm_device(),
|
Backend::Tty(tty) => tty.primary_gbm_device(),
|
||||||
Backend::Winit(_) => None,
|
Backend::Winit(_) => None,
|
||||||
|
Backend::Headless(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +172,7 @@ impl Backend {
|
|||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.set_monitors_active(active),
|
Backend::Tty(tty) => tty.set_monitors_active(active),
|
||||||
Backend::Winit(_) => (),
|
Backend::Winit(_) => (),
|
||||||
|
Backend::Headless(_) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +180,7 @@ impl Backend {
|
|||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.set_output_on_demand_vrr(niri, output, enable_vrr),
|
Backend::Tty(tty) => tty.set_output_on_demand_vrr(niri, output, enable_vrr),
|
||||||
Backend::Winit(_) => (),
|
Backend::Winit(_) => (),
|
||||||
|
Backend::Headless(_) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,13 +188,7 @@ impl Backend {
|
|||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.on_output_config_changed(niri),
|
Backend::Tty(tty) => tty.on_output_config_changed(niri),
|
||||||
Backend::Winit(_) => (),
|
Backend::Winit(_) => (),
|
||||||
}
|
Backend::Headless(_) => (),
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_debug_config_changed(&mut self) {
|
|
||||||
match self {
|
|
||||||
Backend::Tty(tty) => tty.on_debug_config_changed(),
|
|
||||||
Backend::Winit(_) => (),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,4 +215,12 @@ impl Backend {
|
|||||||
panic!("backend is not Winit")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+248
-259
@@ -18,9 +18,10 @@ use smithay::backend::allocator::dmabuf::Dmabuf;
|
|||||||
use smithay::backend::allocator::format::FormatSet;
|
use smithay::backend::allocator::format::FormatSet;
|
||||||
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
||||||
use smithay::backend::allocator::Fourcc;
|
use smithay::backend::allocator::Fourcc;
|
||||||
use smithay::backend::drm::compositor::{DrmCompositor, PrimaryPlaneElement};
|
use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneElement};
|
||||||
|
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
|
||||||
use smithay::backend::drm::{
|
use smithay::backend::drm::{
|
||||||
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType,
|
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType, VrrSupport,
|
||||||
};
|
};
|
||||||
use smithay::backend::egl::context::ContextPriority;
|
use smithay::backend::egl::context::ContextPriority;
|
||||||
use smithay::backend::egl::{EGLDevice, EGLDisplay};
|
use smithay::backend::egl::{EGLDevice, EGLDisplay};
|
||||||
@@ -28,7 +29,7 @@ use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface}
|
|||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::backend::renderer::multigpu::gbm::GbmGlesBackend;
|
use smithay::backend::renderer::multigpu::gbm::GbmGlesBackend;
|
||||||
use smithay::backend::renderer::multigpu::{GpuManager, MultiFrame, MultiRenderer};
|
use smithay::backend::renderer::multigpu::{GpuManager, MultiFrame, MultiRenderer};
|
||||||
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
|
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, RendererSuper};
|
||||||
use smithay::backend::session::libseat::LibSeatSession;
|
use smithay::backend::session::libseat::LibSeatSession;
|
||||||
use smithay::backend::session::{Event as SessionEvent, Session};
|
use smithay::backend::session::{Event as SessionEvent, Session};
|
||||||
use smithay::backend::udev::{self, UdevBackend, UdevEvent};
|
use smithay::backend::udev::{self, UdevBackend, UdevEvent};
|
||||||
@@ -50,6 +51,7 @@ use smithay::wayland::dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlob
|
|||||||
use smithay::wayland::drm_lease::{
|
use smithay::wayland::drm_lease::{
|
||||||
DrmLease, DrmLeaseBuilder, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
|
DrmLease, DrmLeaseBuilder, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
|
||||||
};
|
};
|
||||||
|
use smithay::wayland::presentation::Refresh;
|
||||||
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
|
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
|
||||||
use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
|
use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
|
||||||
use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||||
@@ -63,7 +65,12 @@ use crate::render_helpers::renderer::AsGlesRenderer;
|
|||||||
use crate::render_helpers::{resources, shaders, RenderTarget};
|
use crate::render_helpers::{resources, shaders, RenderTarget};
|
||||||
use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output};
|
use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output};
|
||||||
|
|
||||||
const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888];
|
const SUPPORTED_COLOR_FORMATS: [Fourcc; 4] = [
|
||||||
|
Fourcc::Xrgb8888,
|
||||||
|
Fourcc::Xbgr8888,
|
||||||
|
Fourcc::Argb8888,
|
||||||
|
Fourcc::Abgr8888,
|
||||||
|
];
|
||||||
|
|
||||||
pub struct Tty {
|
pub struct Tty {
|
||||||
config: Rc<RefCell<Config>>,
|
config: Rc<RefCell<Config>>,
|
||||||
@@ -95,19 +102,20 @@ pub type TtyRenderer<'render> = MultiRenderer<
|
|||||||
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
pub type TtyFrame<'render, 'frame> = MultiFrame<
|
pub type TtyFrame<'render, 'frame, 'buffer> = MultiFrame<
|
||||||
'render,
|
'render,
|
||||||
'render,
|
'render,
|
||||||
'frame,
|
'frame,
|
||||||
|
'buffer,
|
||||||
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
||||||
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
pub type TtyRendererError<'render> = <TtyRenderer<'render> as Renderer>::Error;
|
pub type TtyRendererError<'render> = <TtyRenderer<'render> as RendererSuper>::Error;
|
||||||
|
|
||||||
type GbmDrmCompositor = DrmCompositor<
|
type GbmDrmCompositor = DrmCompositor<
|
||||||
GbmAllocator<DrmDeviceFd>,
|
GbmAllocator<DrmDeviceFd>,
|
||||||
GbmDevice<DrmDeviceFd>,
|
GbmFramebufferExporter<DrmDeviceFd>,
|
||||||
(OutputPresentationFeedback, Duration),
|
(OutputPresentationFeedback, Duration),
|
||||||
DrmDeviceFd,
|
DrmDeviceFd,
|
||||||
>;
|
>;
|
||||||
@@ -117,7 +125,7 @@ pub struct OutputDevice {
|
|||||||
render_node: DrmNode,
|
render_node: DrmNode,
|
||||||
drm_scanner: DrmScanner,
|
drm_scanner: DrmScanner,
|
||||||
surfaces: HashMap<crtc::Handle, Surface>,
|
surfaces: HashMap<crtc::Handle, Surface>,
|
||||||
output_ids: HashMap<crtc::Handle, OutputId>,
|
known_crtcs: HashMap<crtc::Handle, CrtcInfo>,
|
||||||
// SAFETY: drop after all the objects used with them are dropped.
|
// SAFETY: drop after all the objects used with them are dropped.
|
||||||
// See https://github.com/Smithay/smithay/issues/1102.
|
// See https://github.com/Smithay/smithay/issues/1102.
|
||||||
drm: DrmDevice,
|
drm: DrmDevice,
|
||||||
@@ -128,6 +136,13 @@ pub struct OutputDevice {
|
|||||||
active_leases: Vec<DrmLease>,
|
active_leases: Vec<DrmLease>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A connected, but not necessarily enabled, crtc.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CrtcInfo {
|
||||||
|
id: OutputId,
|
||||||
|
name: OutputName,
|
||||||
|
}
|
||||||
|
|
||||||
impl OutputDevice {
|
impl OutputDevice {
|
||||||
pub fn lease_request(
|
pub fn lease_request(
|
||||||
&self,
|
&self,
|
||||||
@@ -167,6 +182,35 @@ impl OutputDevice {
|
|||||||
pub fn remove_lease(&mut self, lease_id: u32) {
|
pub fn remove_lease(&mut self, lease_id: u32) {
|
||||||
self.active_leases.retain(|l| l.id() != lease_id);
|
self.active_leases.retain(|l| l.id() != lease_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn known_crtc_name(
|
||||||
|
&self,
|
||||||
|
crtc: &crtc::Handle,
|
||||||
|
conn: &connector::Info,
|
||||||
|
disable_monitor_names: bool,
|
||||||
|
) -> OutputName {
|
||||||
|
if disable_monitor_names {
|
||||||
|
let conn_name = format_connector_name(conn);
|
||||||
|
return OutputName {
|
||||||
|
connector: conn_name,
|
||||||
|
make: None,
|
||||||
|
model: None,
|
||||||
|
serial: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(info) = self.known_crtcs.get(crtc) else {
|
||||||
|
let conn_name = format_connector_name(conn);
|
||||||
|
error!("crtc for connector {conn_name} missing from known");
|
||||||
|
return OutputName {
|
||||||
|
connector: conn_name,
|
||||||
|
make: None,
|
||||||
|
model: None,
|
||||||
|
serial: None,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
info.name.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -183,7 +227,6 @@ struct Surface {
|
|||||||
gamma_props: Option<GammaProps>,
|
gamma_props: Option<GammaProps>,
|
||||||
/// Gamma change to apply upon session resume.
|
/// Gamma change to apply upon session resume.
|
||||||
pending_gamma_change: Option<Option<Vec<u16>>>,
|
pending_gamma_change: Option<Option<Vec<u16>>>,
|
||||||
vrr_enabled: bool,
|
|
||||||
/// Tracy frame that goes from vblank to vblank.
|
/// Tracy frame that goes from vblank to vblank.
|
||||||
vblank_frame: Option<tracy_client::Frame>,
|
vblank_frame: Option<tracy_client::Frame>,
|
||||||
/// Frame name for the VBlank frame.
|
/// Frame name for the VBlank frame.
|
||||||
@@ -404,8 +447,6 @@ impl Tty {
|
|||||||
self.device_changed(node.dev_id(), niri);
|
self.device_changed(node.dev_id(), niri);
|
||||||
|
|
||||||
// Apply pending gamma changes and restore our existing gamma.
|
// Apply pending gamma changes and restore our existing gamma.
|
||||||
//
|
|
||||||
// Also, restore our VRR.
|
|
||||||
let device = self.devices.get_mut(&node).unwrap();
|
let device = self.devices.get_mut(&node).unwrap();
|
||||||
for (crtc, surface) in device.surfaces.iter_mut() {
|
for (crtc, surface) in device.surfaces.iter_mut() {
|
||||||
if let Some(ramp) = surface.pending_gamma_change.take() {
|
if let Some(ramp) = surface.pending_gamma_change.take() {
|
||||||
@@ -423,33 +464,6 @@ impl Tty {
|
|||||||
warn!("error restoring gamma: {err:?}");
|
warn!("error restoring gamma: {err:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore VRR.
|
|
||||||
let output = niri
|
|
||||||
.global_space
|
|
||||||
.outputs()
|
|
||||||
.find(|output| {
|
|
||||||
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
|
|
||||||
tty_state.node == node && tty_state.crtc == *crtc
|
|
||||||
})
|
|
||||||
.cloned();
|
|
||||||
let Some(output) = output else {
|
|
||||||
error!("missing output for crtc: {crtc:?}");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Some(output_state) = niri.output_state.get_mut(&output) else {
|
|
||||||
error!("missing state for output {:?}", surface.name.connector);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
try_to_change_vrr(
|
|
||||||
&device.drm,
|
|
||||||
surface.connector,
|
|
||||||
*crtc,
|
|
||||||
surface,
|
|
||||||
output_state,
|
|
||||||
surface.vrr_enabled,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,7 +480,7 @@ impl Tty {
|
|||||||
|
|
||||||
self.refresh_ipc_outputs(niri);
|
self.refresh_ipc_outputs(niri);
|
||||||
|
|
||||||
niri.idle_notifier_state.notify_activity(&niri.seat);
|
niri.notify_activity();
|
||||||
niri.monitors_active = true;
|
niri.monitors_active = true;
|
||||||
self.set_monitors_active(true);
|
self.set_monitors_active(true);
|
||||||
niri.queue_redraw_all();
|
niri.queue_redraw_all();
|
||||||
@@ -535,7 +549,7 @@ impl Tty {
|
|||||||
}
|
}
|
||||||
drop(config);
|
drop(config);
|
||||||
|
|
||||||
niri.layout.update_shaders();
|
niri.update_shaders();
|
||||||
|
|
||||||
// Create the dmabuf global.
|
// Create the dmabuf global.
|
||||||
let primary_formats = renderer.dmabuf_formats();
|
let primary_formats = renderer.dmabuf_formats();
|
||||||
@@ -596,7 +610,7 @@ impl Tty {
|
|||||||
gbm,
|
gbm,
|
||||||
drm_scanner: DrmScanner::new(),
|
drm_scanner: DrmScanner::new(),
|
||||||
surfaces: HashMap::new(),
|
surfaces: HashMap::new(),
|
||||||
output_ids: HashMap::new(),
|
known_crtcs: HashMap::new(),
|
||||||
drm_lease_state,
|
drm_lease_state,
|
||||||
active_leases: Vec::new(),
|
active_leases: Vec::new(),
|
||||||
non_desktop_connectors: HashSet::new(),
|
non_desktop_connectors: HashSet::new(),
|
||||||
@@ -629,6 +643,7 @@ impl Tty {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut added = Vec::new();
|
||||||
let mut removed = Vec::new();
|
let mut removed = Vec::new();
|
||||||
for event in scan_result {
|
for event in scan_result {
|
||||||
match event {
|
match event {
|
||||||
@@ -637,16 +652,16 @@ impl Tty {
|
|||||||
crtc: Some(crtc),
|
crtc: Some(crtc),
|
||||||
} => {
|
} => {
|
||||||
let connector_name = format_connector_name(&connector);
|
let connector_name = format_connector_name(&connector);
|
||||||
let output_name =
|
let name = make_output_name(&device.drm, connector.handle(), connector_name);
|
||||||
make_output_name(&device.drm, connector.handle(), connector_name, false);
|
|
||||||
debug!(
|
debug!(
|
||||||
"new connector: {} \"{}\"",
|
"new connector: {} \"{}\"",
|
||||||
&output_name.connector,
|
&name.connector,
|
||||||
output_name.format_make_model_serial(),
|
name.format_make_model_serial(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Assign an id to this crtc.
|
// Assign an id to this crtc.
|
||||||
device.output_ids.insert(crtc, OutputId::next());
|
let id = OutputId::next();
|
||||||
|
added.push((crtc, CrtcInfo { id, name }));
|
||||||
}
|
}
|
||||||
DrmScanEvent::Disconnected {
|
DrmScanEvent::Disconnected {
|
||||||
crtc: Some(crtc), ..
|
crtc: Some(crtc), ..
|
||||||
@@ -667,11 +682,42 @@ impl Tty {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for crtc in removed {
|
for crtc in removed {
|
||||||
if device.output_ids.remove(&crtc).is_none() {
|
if device.known_crtcs.remove(&crtc).is_none() {
|
||||||
error!("output ID missing for disconnected crtc: {crtc:?}");
|
error!("output ID missing for disconnected crtc: {crtc:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (crtc, mut info) in added {
|
||||||
|
// Make/model/serial can match exactly between different physical monitors. This doesn't
|
||||||
|
// happen often, but our Layout does not support such duplicates and will panic.
|
||||||
|
//
|
||||||
|
// As a workaround, search for duplicates, and unname the new connectors if one is
|
||||||
|
// found. Connector names are always unique.
|
||||||
|
let name = &mut info.name;
|
||||||
|
let formatted = name.format_make_model_serial_or_connector();
|
||||||
|
for info in self.devices.values().flat_map(|d| d.known_crtcs.values()) {
|
||||||
|
if info.name.matches(&formatted) {
|
||||||
|
let connector = mem::take(&mut name.connector);
|
||||||
|
warn!(
|
||||||
|
"new connector {connector} duplicates make/model/serial \
|
||||||
|
of existing connector {}, unnaming",
|
||||||
|
info.name.connector,
|
||||||
|
);
|
||||||
|
*name = OutputName {
|
||||||
|
connector,
|
||||||
|
make: None,
|
||||||
|
model: None,
|
||||||
|
serial: None,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert it right away so next added connector will check against this one too.
|
||||||
|
let device = self.devices.get_mut(&node).unwrap();
|
||||||
|
device.known_crtcs.insert(crtc, info);
|
||||||
|
}
|
||||||
|
|
||||||
// This will connect any new connectors if needed, and apply other changes, such as
|
// This will connect any new connectors if needed, and apply other changes, such as
|
||||||
// connecting back the internal laptop monitor once it becomes the only monitor left.
|
// connecting back the internal laptop monitor once it becomes the only monitor left.
|
||||||
//
|
//
|
||||||
@@ -763,12 +809,8 @@ impl Tty {
|
|||||||
|
|
||||||
let device = self.devices.get_mut(&node).context("missing device")?;
|
let device = self.devices.get_mut(&node).context("missing device")?;
|
||||||
|
|
||||||
let output_name = make_output_name(
|
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
|
||||||
&device.drm,
|
let output_name = device.known_crtc_name(&crtc, &connector, disable_monitor_names);
|
||||||
connector.handle(),
|
|
||||||
connector_name.clone(),
|
|
||||||
self.config.borrow().debug.disable_monitor_names,
|
|
||||||
);
|
|
||||||
|
|
||||||
let non_desktop = find_drm_property(&device.drm, connector.handle(), "non-desktop")
|
let non_desktop = find_drm_property(&device.drm, connector.handle(), "non-desktop")
|
||||||
.and_then(|(_, info, value)| info.value_type().convert_value(value).as_boolean())
|
.and_then(|(_, info, value)| info.value_type().convert_value(value).as_boolean())
|
||||||
@@ -821,45 +863,6 @@ impl Tty {
|
|||||||
Err(err) => debug!("error setting max bpc: {err:?}"),
|
Err(err) => debug!("error setting max bpc: {err:?}"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to enable VRR if requested.
|
|
||||||
let mut vrr_enabled = false;
|
|
||||||
if let Some(capable) = is_vrr_capable(&device.drm, connector.handle()) {
|
|
||||||
if capable {
|
|
||||||
// Even if on-demand, we still disable it until later checks.
|
|
||||||
let vrr = config.is_vrr_always_on();
|
|
||||||
let word = if vrr { "enabling" } else { "disabling" };
|
|
||||||
|
|
||||||
match set_vrr_enabled(&device.drm, crtc, vrr) {
|
|
||||||
Ok(enabled) => {
|
|
||||||
if enabled != vrr {
|
|
||||||
warn!("failed {} VRR", word);
|
|
||||||
}
|
|
||||||
|
|
||||||
vrr_enabled = enabled;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("error {} VRR: {err:?}", word);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !config.is_vrr_always_off() {
|
|
||||||
warn!("cannot enable VRR because connector is not vrr_capable");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to disable it anyway to work around a bug where resetting DRM state causes
|
|
||||||
// vrr_capable to be reset to 0, potentially leaving VRR_ENABLED at 1.
|
|
||||||
let res = set_vrr_enabled(&device.drm, crtc, false);
|
|
||||||
if matches!(res, Ok(true)) {
|
|
||||||
warn!("error disabling VRR");
|
|
||||||
|
|
||||||
// So that we can try it again later.
|
|
||||||
vrr_enabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if !config.is_vrr_always_off() {
|
|
||||||
warn!("cannot enable VRR because connector is not vrr_capable");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut gamma_props = GammaProps::new(&device.drm, crtc)
|
let mut gamma_props = GammaProps::new(&device.drm, crtc)
|
||||||
.map_err(|err| debug!("error getting gamma properties: {err:?}"))
|
.map_err(|err| debug!("error getting gamma properties: {err:?}"))
|
||||||
.ok();
|
.ok();
|
||||||
@@ -878,6 +881,31 @@ impl Tty {
|
|||||||
.drm
|
.drm
|
||||||
.create_surface(crtc, mode, &[connector.handle()])?;
|
.create_surface(crtc, mode, &[connector.handle()])?;
|
||||||
|
|
||||||
|
// Try to enable VRR if requested.
|
||||||
|
match surface.vrr_supported(connector.handle()) {
|
||||||
|
Ok(VrrSupport::Supported | VrrSupport::RequiresModeset) => {
|
||||||
|
// Even if on-demand, we still disable it until later checks.
|
||||||
|
let vrr = config.is_vrr_always_on();
|
||||||
|
let word = if vrr { "enabling" } else { "disabling" };
|
||||||
|
|
||||||
|
if let Err(err) = surface.use_vrr(vrr) {
|
||||||
|
warn!("error {} VRR: {err:?}", word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(VrrSupport::NotSupported) => {
|
||||||
|
if !config.is_vrr_always_off() {
|
||||||
|
warn!("cannot enable VRR because connector does not support it");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to disable it anyway to work around a bug where resetting DRM state causes
|
||||||
|
// vrr_capable to be reset to 0, potentially leaving VRR_ENABLED at 1.
|
||||||
|
let _ = surface.use_vrr(false);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error querying for VRR support: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create GBM allocator.
|
// Create GBM allocator.
|
||||||
let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT;
|
let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT;
|
||||||
let allocator = GbmAllocator::new(device.gbm.clone(), gbm_flags);
|
let allocator = GbmAllocator::new(device.gbm.clone(), gbm_flags);
|
||||||
@@ -904,23 +932,6 @@ impl Tty {
|
|||||||
.insert_if_missing(|| TtyOutputState { node, crtc });
|
.insert_if_missing(|| TtyOutputState { node, crtc });
|
||||||
output.user_data().insert_if_missing(|| output_name.clone());
|
output.user_data().insert_if_missing(|| output_name.clone());
|
||||||
|
|
||||||
let mut planes = surface.planes().clone();
|
|
||||||
|
|
||||||
let config = self.config.borrow();
|
|
||||||
|
|
||||||
// Overlay planes are disabled by default as they cause weird performance issues on my
|
|
||||||
// system.
|
|
||||||
if !config.debug.enable_overlay_planes {
|
|
||||||
planes.overlay.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cursor planes have bugs on some systems.
|
|
||||||
let cursor_plane_gbm = if config.debug.disable_cursor_plane {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(device.gbm.clone())
|
|
||||||
};
|
|
||||||
|
|
||||||
let renderer = self.gpu_manager.single_renderer(&device.render_node)?;
|
let renderer = self.gpu_manager.single_renderer(&device.render_node)?;
|
||||||
let egl_context = renderer.as_ref().egl_context();
|
let egl_context = renderer.as_ref().egl_context();
|
||||||
let render_formats = egl_context.dmabuf_render_formats();
|
let render_formats = egl_context.dmabuf_render_formats();
|
||||||
@@ -959,15 +970,15 @@ impl Tty {
|
|||||||
let res = DrmCompositor::new(
|
let res = DrmCompositor::new(
|
||||||
OutputModeSource::Auto(output.clone()),
|
OutputModeSource::Auto(output.clone()),
|
||||||
surface,
|
surface,
|
||||||
Some(planes),
|
None,
|
||||||
allocator.clone(),
|
allocator.clone(),
|
||||||
device.gbm.clone(),
|
GbmFramebufferExporter::new(device.gbm.clone()),
|
||||||
SUPPORTED_COLOR_FORMATS,
|
SUPPORTED_COLOR_FORMATS,
|
||||||
// This is only used to pick a good internal format, so it can use the surface's render
|
// This is only used to pick a good internal format, so it can use the surface's render
|
||||||
// formats, even though we only ever render on the primary GPU.
|
// formats, even though we only ever render on the primary GPU.
|
||||||
render_formats.clone(),
|
render_formats.clone(),
|
||||||
device.drm.cursor_size(),
|
device.drm.cursor_size(),
|
||||||
cursor_plane_gbm.clone(),
|
Some(device.gbm.clone()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut compositor = match res {
|
let mut compositor = match res {
|
||||||
@@ -985,21 +996,17 @@ impl Tty {
|
|||||||
let surface = device
|
let surface = device
|
||||||
.drm
|
.drm
|
||||||
.create_surface(crtc, mode, &[connector.handle()])?;
|
.create_surface(crtc, mode, &[connector.handle()])?;
|
||||||
let mut planes = surface.planes().clone();
|
|
||||||
if !config.debug.enable_overlay_planes {
|
|
||||||
planes.overlay.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
DrmCompositor::new(
|
DrmCompositor::new(
|
||||||
OutputModeSource::Auto(output.clone()),
|
OutputModeSource::Auto(output.clone()),
|
||||||
surface,
|
surface,
|
||||||
Some(planes),
|
None,
|
||||||
allocator,
|
allocator,
|
||||||
device.gbm.clone(),
|
GbmFramebufferExporter::new(device.gbm.clone()),
|
||||||
SUPPORTED_COLOR_FORMATS,
|
SUPPORTED_COLOR_FORMATS,
|
||||||
render_formats,
|
render_formats,
|
||||||
device.drm.cursor_size(),
|
device.drm.cursor_size(),
|
||||||
cursor_plane_gbm,
|
Some(device.gbm.clone()),
|
||||||
)
|
)
|
||||||
.context("error creating DRM compositor")?
|
.context("error creating DRM compositor")?
|
||||||
}
|
}
|
||||||
@@ -1008,7 +1015,6 @@ impl Tty {
|
|||||||
if self.debug_tint {
|
if self.debug_tint {
|
||||||
compositor.set_debug_flags(DebugFlags::TINT);
|
compositor.set_debug_flags(DebugFlags::TINT);
|
||||||
}
|
}
|
||||||
compositor.use_direct_scanout(!config.debug.disable_direct_scanout);
|
|
||||||
|
|
||||||
let mut dmabuf_feedback = None;
|
let mut dmabuf_feedback = None;
|
||||||
if let Ok(primary_renderer) = self.gpu_manager.single_renderer(&self.primary_render_node) {
|
if let Ok(primary_renderer) = self.gpu_manager.single_renderer(&self.primary_render_node) {
|
||||||
@@ -1037,6 +1043,8 @@ impl Tty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let vrr_enabled = compositor.vrr_enabled();
|
||||||
|
|
||||||
let vblank_frame_name =
|
let vblank_frame_name =
|
||||||
tracy_client::FrameName::new_leak(format!("vblank on {connector_name}"));
|
tracy_client::FrameName::new_leak(format!("vblank on {connector_name}"));
|
||||||
let time_since_presentation_plot_name = tracy_client::PlotName::new_leak(format!(
|
let time_since_presentation_plot_name = tracy_client::PlotName::new_leak(format!(
|
||||||
@@ -1054,7 +1062,6 @@ impl Tty {
|
|||||||
compositor,
|
compositor,
|
||||||
dmabuf_feedback,
|
dmabuf_feedback,
|
||||||
gamma_props,
|
gamma_props,
|
||||||
vrr_enabled,
|
|
||||||
pending_gamma_change: None,
|
pending_gamma_change: None,
|
||||||
vblank_frame: None,
|
vblank_frame: None,
|
||||||
vblank_frame_name,
|
vblank_frame_name,
|
||||||
@@ -1233,10 +1240,17 @@ impl Tty {
|
|||||||
// Mark the last frame as submitted.
|
// Mark the last frame as submitted.
|
||||||
match surface.compositor.frame_submitted() {
|
match surface.compositor.frame_submitted() {
|
||||||
Ok(Some((mut feedback, target_presentation_time))) => {
|
Ok(Some((mut feedback, target_presentation_time))) => {
|
||||||
let refresh = output_state
|
let refresh = match output_state.frame_clock.refresh_interval() {
|
||||||
.frame_clock
|
Some(refresh) => {
|
||||||
.refresh_interval()
|
if output_state.frame_clock.vrr() {
|
||||||
.unwrap_or(Duration::ZERO);
|
Refresh::Variable(refresh)
|
||||||
|
} else {
|
||||||
|
Refresh::Fixed(refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Refresh::Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
// FIXME: ideally should be monotonically increasing for a surface.
|
// FIXME: ideally should be monotonically increasing for a surface.
|
||||||
let seq = meta.sequence as u64;
|
let seq = meta.sequence as u64;
|
||||||
let mut flags = wp_presentation_feedback::Kind::Vsync
|
let mut flags = wp_presentation_feedback::Kind::Vsync
|
||||||
@@ -1386,9 +1400,35 @@ impl Tty {
|
|||||||
draw_damage(&mut output_state.debug_damage_tracker, &mut elements);
|
draw_damage(&mut output_state.debug_damage_tracker, &mut elements);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Overlay planes are disabled by default as they cause weird performance issues on my
|
||||||
|
// system.
|
||||||
|
let flags = {
|
||||||
|
let debug = &self.config.borrow().debug;
|
||||||
|
|
||||||
|
let primary_scanout_flag = if debug.restrict_primary_scanout_to_matching_format {
|
||||||
|
FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT
|
||||||
|
} else {
|
||||||
|
FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT_ANY
|
||||||
|
};
|
||||||
|
let mut flags = primary_scanout_flag | FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT;
|
||||||
|
|
||||||
|
if debug.enable_overlay_planes {
|
||||||
|
flags.insert(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT);
|
||||||
|
}
|
||||||
|
if debug.disable_direct_scanout {
|
||||||
|
flags.remove(primary_scanout_flag);
|
||||||
|
flags.remove(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT);
|
||||||
|
}
|
||||||
|
if debug.disable_cursor_plane {
|
||||||
|
flags.remove(FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
flags
|
||||||
|
};
|
||||||
|
|
||||||
// Hand them over to the DRM.
|
// Hand them over to the DRM.
|
||||||
let drm_compositor = &mut surface.compositor;
|
let drm_compositor = &mut surface.compositor;
|
||||||
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4]) {
|
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4], flags) {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
let needs_sync = res.needs_sync()
|
let needs_sync = res.needs_sync()
|
||||||
|| self
|
|| self
|
||||||
@@ -1569,17 +1609,13 @@ impl Tty {
|
|||||||
let _span = tracy_client::span!("Tty::refresh_ipc_outputs");
|
let _span = tracy_client::span!("Tty::refresh_ipc_outputs");
|
||||||
|
|
||||||
let mut ipc_outputs = HashMap::new();
|
let mut ipc_outputs = HashMap::new();
|
||||||
|
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
|
||||||
|
|
||||||
for (node, device) in &self.devices {
|
for (node, device) in &self.devices {
|
||||||
for (connector, crtc) in device.drm_scanner.crtcs() {
|
for (connector, crtc) in device.drm_scanner.crtcs() {
|
||||||
let connector_name = format_connector_name(connector);
|
let connector_name = format_connector_name(connector);
|
||||||
let physical_size = connector.size();
|
let physical_size = connector.size();
|
||||||
let output_name = make_output_name(
|
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
|
||||||
&device.drm,
|
|
||||||
connector.handle(),
|
|
||||||
connector_name.clone(),
|
|
||||||
self.config.borrow().debug.disable_monitor_names,
|
|
||||||
);
|
|
||||||
|
|
||||||
let surface = device.surfaces.get(&crtc);
|
let surface = device.surfaces.get(&crtc);
|
||||||
let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode());
|
let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode());
|
||||||
@@ -1614,8 +1650,17 @@ impl Tty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let vrr_supported = is_vrr_capable(&device.drm, connector.handle()) == Some(true);
|
let vrr_supported = surface
|
||||||
let vrr_enabled = surface.map_or(false, |surface| surface.vrr_enabled);
|
.map(|surface| {
|
||||||
|
matches!(
|
||||||
|
surface.compositor.vrr_supported(connector.handle()),
|
||||||
|
Ok(VrrSupport::Supported | VrrSupport::RequiresModeset)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
is_vrr_capable(&device.drm, connector.handle()) == Some(true)
|
||||||
|
});
|
||||||
|
let vrr_enabled = surface.is_some_and(|surface| surface.compositor.vrr_enabled());
|
||||||
|
|
||||||
let logical = niri
|
let logical = niri
|
||||||
.global_space
|
.global_space
|
||||||
@@ -1626,6 +1671,12 @@ impl Tty {
|
|||||||
})
|
})
|
||||||
.map(logical_output);
|
.map(logical_output);
|
||||||
|
|
||||||
|
let id = device.known_crtcs.get(&crtc).map(|info| info.id);
|
||||||
|
let id = id.unwrap_or_else(|| {
|
||||||
|
error!("crtc for connector {connector_name} missing from known");
|
||||||
|
OutputId::next()
|
||||||
|
});
|
||||||
|
|
||||||
let ipc_output = niri_ipc::Output {
|
let ipc_output = niri_ipc::Output {
|
||||||
name: connector_name,
|
name: connector_name,
|
||||||
make: output_name.make.unwrap_or_else(|| "Unknown".into()),
|
make: output_name.make.unwrap_or_else(|| "Unknown".into()),
|
||||||
@@ -1639,10 +1690,6 @@ impl Tty {
|
|||||||
logical,
|
logical,
|
||||||
};
|
};
|
||||||
|
|
||||||
let id = device.output_ids.get(&crtc).copied().unwrap_or_else(|| {
|
|
||||||
error!("output ID missing for crtc: {crtc:?}");
|
|
||||||
OutputId::next()
|
|
||||||
});
|
|
||||||
ipc_outputs.insert(id, ipc_output);
|
ipc_outputs.insert(id, ipc_output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1700,14 +1747,17 @@ impl Tty {
|
|||||||
for (&crtc, surface) in device.surfaces.iter_mut() {
|
for (&crtc, surface) in device.surfaces.iter_mut() {
|
||||||
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
|
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
|
||||||
if tty_state.node == node && tty_state.crtc == crtc {
|
if tty_state.node == node && tty_state.crtc == crtc {
|
||||||
try_to_change_vrr(
|
let word = if enable_vrr { "enabling" } else { "disabling" };
|
||||||
&device.drm,
|
if let Err(err) = surface.compositor.use_vrr(enable_vrr) {
|
||||||
surface.connector,
|
warn!(
|
||||||
crtc,
|
"output {:?}: error {} VRR: {err:?}",
|
||||||
surface,
|
surface.name.connector, word
|
||||||
output_state,
|
);
|
||||||
enable_vrr,
|
}
|
||||||
);
|
output_state
|
||||||
|
.frame_clock
|
||||||
|
.set_vrr(surface.compositor.vrr_enabled());
|
||||||
|
|
||||||
self.refresh_ipc_outputs(niri);
|
self.refresh_ipc_outputs(niri);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1773,8 +1823,11 @@ impl Tty {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let change_mode = surface.compositor.pending_mode() != mode;
|
let change_mode = surface.compositor.pending_mode() != mode;
|
||||||
let change_always_vrr = surface.vrr_enabled != config.is_vrr_always_on();
|
|
||||||
|
let vrr_enabled = surface.compositor.vrr_enabled();
|
||||||
|
let change_always_vrr = vrr_enabled != config.is_vrr_always_on();
|
||||||
let is_on_demand_vrr = config.is_vrr_on_demand();
|
let is_on_demand_vrr = config.is_vrr_on_demand();
|
||||||
|
|
||||||
if !change_mode && !change_always_vrr && !is_on_demand_vrr {
|
if !change_mode && !change_always_vrr && !is_on_demand_vrr {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1796,17 +1849,20 @@ impl Tty {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (is_on_demand_vrr && surface.vrr_enabled != output_state.on_demand_vrr_enabled)
|
if (is_on_demand_vrr && vrr_enabled != output_state.on_demand_vrr_enabled)
|
||||||
|| (!is_on_demand_vrr && change_always_vrr)
|
|| (!is_on_demand_vrr && change_always_vrr)
|
||||||
{
|
{
|
||||||
try_to_change_vrr(
|
let vrr = !vrr_enabled;
|
||||||
&device.drm,
|
let word = if vrr { "enabling" } else { "disabling" };
|
||||||
connector.handle(),
|
if let Err(err) = surface.compositor.use_vrr(vrr) {
|
||||||
crtc,
|
warn!(
|
||||||
surface,
|
"output {:?}: error {} VRR: {err:?}",
|
||||||
output_state,
|
surface.name.connector, word
|
||||||
!surface.vrr_enabled,
|
);
|
||||||
);
|
}
|
||||||
|
output_state
|
||||||
|
.frame_clock
|
||||||
|
.set_vrr(surface.compositor.vrr_enabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
if change_mode {
|
if change_mode {
|
||||||
@@ -1838,12 +1894,17 @@ impl Tty {
|
|||||||
let wl_mode = Mode::from(mode);
|
let wl_mode = Mode::from(mode);
|
||||||
output.change_current_state(Some(wl_mode), None, None, None);
|
output.change_current_state(Some(wl_mode), None, None, None);
|
||||||
output.set_preferred(wl_mode);
|
output.set_preferred(wl_mode);
|
||||||
output_state.frame_clock =
|
output_state.frame_clock = FrameClock::new(
|
||||||
FrameClock::new(Some(refresh_interval(mode)), surface.vrr_enabled);
|
Some(refresh_interval(mode)),
|
||||||
|
surface.compositor.vrr_enabled(),
|
||||||
|
);
|
||||||
niri.output_resized(&output);
|
niri.output_resized(&output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let config = self.config.borrow();
|
||||||
|
let disable_monitor_names = config.debug.disable_monitor_names;
|
||||||
|
|
||||||
for (connector, crtc) in device.drm_scanner.crtcs() {
|
for (connector, crtc) in device.drm_scanner.crtcs() {
|
||||||
// Check if connected.
|
// Check if connected.
|
||||||
if connector.state() != connector::State::Connected {
|
if connector.state() != connector::State::Connected {
|
||||||
@@ -1859,16 +1920,9 @@ impl Tty {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let connector_name = format_connector_name(connector);
|
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
|
||||||
let output_name = make_output_name(
|
|
||||||
&device.drm,
|
let config = config
|
||||||
connector.handle(),
|
|
||||||
connector_name,
|
|
||||||
self.config.borrow().debug.disable_monitor_names,
|
|
||||||
);
|
|
||||||
let config = self
|
|
||||||
.config
|
|
||||||
.borrow()
|
|
||||||
.outputs
|
.outputs
|
||||||
.find(&output_name)
|
.find(&output_name)
|
||||||
.cloned()
|
.cloned()
|
||||||
@@ -1897,24 +1951,12 @@ impl Tty {
|
|||||||
self.refresh_ipc_outputs(niri);
|
self.refresh_ipc_outputs(niri);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_debug_config_changed(&mut self) {
|
|
||||||
let config = self.config.borrow();
|
|
||||||
let debug = &config.debug;
|
|
||||||
let use_direct_scanout = !debug.disable_direct_scanout;
|
|
||||||
|
|
||||||
// FIXME: reload other flags if possible?
|
|
||||||
for device in self.devices.values_mut() {
|
|
||||||
for surface in device.surfaces.values_mut() {
|
|
||||||
surface.compositor.use_direct_scanout(use_direct_scanout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> {
|
pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> {
|
||||||
self.devices.get_mut(&node)
|
self.devices.get_mut(&node)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disconnected_connector_name_by_name_match(&self, target: &str) -> Option<OutputName> {
|
pub fn disconnected_connector_name_by_name_match(&self, target: &str) -> Option<OutputName> {
|
||||||
|
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
|
||||||
for device in self.devices.values() {
|
for device in self.devices.values() {
|
||||||
for (connector, crtc) in device.drm_scanner.crtcs() {
|
for (connector, crtc) in device.drm_scanner.crtcs() {
|
||||||
// Check if connected.
|
// Check if connected.
|
||||||
@@ -1931,13 +1973,7 @@ impl Tty {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let connector_name = format_connector_name(connector);
|
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
|
||||||
let output_name = make_output_name(
|
|
||||||
&device.drm,
|
|
||||||
connector.handle(),
|
|
||||||
connector_name,
|
|
||||||
self.config.borrow().debug.disable_monitor_names,
|
|
||||||
);
|
|
||||||
if output_name.matches(target) {
|
if output_name.matches(target) {
|
||||||
return Some(output_name);
|
return Some(output_name);
|
||||||
}
|
}
|
||||||
@@ -2132,9 +2168,8 @@ fn surface_dmabuf_feedback(
|
|||||||
let surface = compositor.surface();
|
let surface = compositor.surface();
|
||||||
let planes = surface.planes();
|
let planes = surface.planes();
|
||||||
|
|
||||||
let plane_formats = surface
|
let primary_plane_formats = surface.plane_info().formats.clone();
|
||||||
.plane_info()
|
let primary_or_overlay_plane_formats = primary_plane_formats
|
||||||
.formats
|
|
||||||
.iter()
|
.iter()
|
||||||
.chain(planes.overlay.iter().flat_map(|p| p.formats.iter()))
|
.chain(planes.overlay.iter().flat_map(|p| p.formats.iter()))
|
||||||
.copied()
|
.copied()
|
||||||
@@ -2142,7 +2177,11 @@ fn surface_dmabuf_feedback(
|
|||||||
|
|
||||||
// We limit the scan-out trache to formats we can also render from so that there is always a
|
// We limit the scan-out trache to formats we can also render from so that there is always a
|
||||||
// fallback render path available in case the supplied buffer can not be scanned out directly.
|
// fallback render path available in case the supplied buffer can not be scanned out directly.
|
||||||
let mut scanout_formats = plane_formats
|
let mut primary_scanout_formats = primary_plane_formats
|
||||||
|
.intersection(&primary_formats)
|
||||||
|
.copied()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut primary_or_overlay_scanout_formats = primary_or_overlay_plane_formats
|
||||||
.intersection(&primary_formats)
|
.intersection(&primary_formats)
|
||||||
.copied()
|
.copied()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -2150,17 +2189,32 @@ fn surface_dmabuf_feedback(
|
|||||||
// HACK: AMD iGPU + dGPU systems share some modifiers between the two, and yet cross-device
|
// HACK: AMD iGPU + dGPU systems share some modifiers between the two, and yet cross-device
|
||||||
// buffers produce a glitched scanout if the modifier is not Linear...
|
// buffers produce a glitched scanout if the modifier is not Linear...
|
||||||
if primary_render_node != surface_render_node {
|
if primary_render_node != surface_render_node {
|
||||||
scanout_formats.retain(|f| f.modifier == Modifier::Linear);
|
primary_scanout_formats.retain(|f| f.modifier == Modifier::Linear);
|
||||||
|
primary_or_overlay_scanout_formats.retain(|f| f.modifier == Modifier::Linear);
|
||||||
}
|
}
|
||||||
|
|
||||||
let builder = DmabufFeedbackBuilder::new(primary_render_node.dev_id(), primary_formats);
|
let builder = DmabufFeedbackBuilder::new(primary_render_node.dev_id(), primary_formats);
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"primary scanout formats: {}, overlay adds: {}",
|
||||||
|
primary_scanout_formats.len(),
|
||||||
|
primary_or_overlay_scanout_formats.len() - primary_scanout_formats.len(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Prefer the primary-plane-only formats, then primary-or-overlay-plane formats. This will
|
||||||
|
// increase the chance of scanning out a client even with our disabled-by-default overlay
|
||||||
|
// planes.
|
||||||
let scanout = builder
|
let scanout = builder
|
||||||
.clone()
|
.clone()
|
||||||
.add_preference_tranche(
|
.add_preference_tranche(
|
||||||
surface_render_node.dev_id(),
|
surface_render_node.dev_id(),
|
||||||
Some(TrancheFlags::Scanout),
|
Some(TrancheFlags::Scanout),
|
||||||
scanout_formats,
|
primary_scanout_formats,
|
||||||
|
)
|
||||||
|
.add_preference_tranche(
|
||||||
|
surface_render_node.dev_id(),
|
||||||
|
Some(TrancheFlags::Scanout),
|
||||||
|
primary_or_overlay_scanout_formats,
|
||||||
)
|
)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
@@ -2423,24 +2477,6 @@ fn is_vrr_capable(device: &DrmDevice, connector: connector::Handle) -> Option<bo
|
|||||||
info.value_type().convert_value(value).as_boolean()
|
info.value_type().convert_value(value).as_boolean()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_vrr_enabled(device: &DrmDevice, crtc: crtc::Handle, enabled: bool) -> anyhow::Result<bool> {
|
|
||||||
let (prop, info, _) =
|
|
||||||
find_drm_property(device, crtc, "VRR_ENABLED").context("VRR_ENABLED property missing")?;
|
|
||||||
|
|
||||||
let value = property::Value::UnsignedRange(if enabled { 1 } else { 0 });
|
|
||||||
device
|
|
||||||
.set_property(crtc, prop, value.into())
|
|
||||||
.context("error setting VRR_ENABLED property")?;
|
|
||||||
|
|
||||||
let value = get_drm_property(device, crtc, prop)
|
|
||||||
.context("VRR_ENABLED property missing after setting")?;
|
|
||||||
match info.value_type().convert_value(value) {
|
|
||||||
property::Value::UnsignedRange(value) => Ok(value == 1),
|
|
||||||
property::Value::Boolean(value) => Ok(value),
|
|
||||||
_ => bail!("wrong VRR_ENABLED property type"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_gamma_for_crtc(
|
pub fn set_gamma_for_crtc(
|
||||||
device: &DrmDevice,
|
device: &DrmDevice,
|
||||||
crtc: crtc::Handle,
|
crtc: crtc::Handle,
|
||||||
@@ -2486,43 +2522,6 @@ pub fn set_gamma_for_crtc(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_to_change_vrr(
|
|
||||||
device: &DrmDevice,
|
|
||||||
connector: connector::Handle,
|
|
||||||
crtc: crtc::Handle,
|
|
||||||
surface: &mut Surface,
|
|
||||||
output_state: &mut crate::niri::OutputState,
|
|
||||||
enable_vrr: bool,
|
|
||||||
) {
|
|
||||||
let _span = tracy_client::span!("try_to_change_vrr");
|
|
||||||
|
|
||||||
if is_vrr_capable(device, connector) == Some(true) {
|
|
||||||
let word = if enable_vrr { "enabling" } else { "disabling" };
|
|
||||||
|
|
||||||
match set_vrr_enabled(device, crtc, enable_vrr) {
|
|
||||||
Ok(enabled) => {
|
|
||||||
if enabled != enable_vrr {
|
|
||||||
warn!("output {:?}: failed {} VRR", surface.name.connector, word);
|
|
||||||
}
|
|
||||||
|
|
||||||
surface.vrr_enabled = enabled;
|
|
||||||
output_state.frame_clock.set_vrr(enabled);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!(
|
|
||||||
"output {:?}: error {} VRR: {err:?}",
|
|
||||||
surface.name.connector, word
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if enable_vrr {
|
|
||||||
warn!(
|
|
||||||
"output {:?}: cannot enable VRR because connector is not vrr_capable",
|
|
||||||
surface.name.connector
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_connector_name(connector: &connector::Info) -> String {
|
fn format_connector_name(connector: &connector::Info) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{}-{}",
|
"{}-{}",
|
||||||
@@ -2535,17 +2534,7 @@ fn make_output_name(
|
|||||||
device: &DrmDevice,
|
device: &DrmDevice,
|
||||||
connector: connector::Handle,
|
connector: connector::Handle,
|
||||||
connector_name: String,
|
connector_name: String,
|
||||||
disable_monitor_names: bool,
|
|
||||||
) -> OutputName {
|
) -> OutputName {
|
||||||
if disable_monitor_names {
|
|
||||||
return OutputName {
|
|
||||||
connector: connector_name,
|
|
||||||
make: None,
|
|
||||||
model: None,
|
|
||||||
serial: None,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let info = get_edid_info(device, connector)
|
let info = get_edid_info(device, connector)
|
||||||
.map_err(|err| warn!("error getting EDID info for {connector_name}: {err:?}"))
|
.map_err(|err| warn!("error getting EDID info for {connector_name}: {err:?}"))
|
||||||
.ok();
|
.ok();
|
||||||
|
|||||||
+13
-11
@@ -3,7 +3,6 @@ use std::collections::HashMap;
|
|||||||
use std::mem;
|
use std::mem;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use niri_config::{Config, OutputName};
|
use niri_config::{Config, OutputName};
|
||||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||||
@@ -16,6 +15,7 @@ use smithay::reexports::calloop::LoopHandle;
|
|||||||
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||||
use smithay::reexports::winit::dpi::LogicalSize;
|
use smithay::reexports::winit::dpi::LogicalSize;
|
||||||
use smithay::reexports::winit::window::Window;
|
use smithay::reexports::winit::window::Window;
|
||||||
|
use smithay::wayland::presentation::Refresh;
|
||||||
|
|
||||||
use super::{IpcOutputMap, OutputId, RenderResult};
|
use super::{IpcOutputMap, OutputId, RenderResult};
|
||||||
use crate::niri::{Niri, RedrawState, State};
|
use crate::niri::{Niri, RedrawState, State};
|
||||||
@@ -156,7 +156,7 @@ impl Winit {
|
|||||||
}
|
}
|
||||||
drop(config);
|
drop(config);
|
||||||
|
|
||||||
niri.layout.update_shaders();
|
niri.update_shaders();
|
||||||
|
|
||||||
niri.add_output(self.output.clone(), None, false);
|
niri.add_output(self.output.clone(), None, false);
|
||||||
}
|
}
|
||||||
@@ -190,12 +190,16 @@ impl Winit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hand them over to winit.
|
// Hand them over to winit.
|
||||||
self.backend.bind().unwrap();
|
let res = {
|
||||||
let age = self.backend.buffer_age().unwrap();
|
let (renderer, mut framebuffer) = self.backend.bind().unwrap();
|
||||||
let res = self
|
// FIXME: currently impossible to call due to a mutable borrow.
|
||||||
.damage_tracker
|
//
|
||||||
.render_output(self.backend.renderer(), age, &elements, [0.; 4])
|
// let age = self.backend.buffer_age().unwrap();
|
||||||
.unwrap();
|
let age = 0;
|
||||||
|
self.damage_tracker
|
||||||
|
.render_output(renderer, &mut framebuffer, age, &elements, [0.; 4])
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
niri.update_primary_scanout_output(output, &res.states);
|
niri.update_primary_scanout_output(output, &res.states);
|
||||||
|
|
||||||
@@ -216,11 +220,9 @@ impl Winit {
|
|||||||
self.backend.submit(Some(damage)).unwrap();
|
self.backend.submit(Some(damage)).unwrap();
|
||||||
|
|
||||||
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &res.states);
|
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>(
|
presentation_feedbacks.presented::<_, smithay::utils::Monotonic>(
|
||||||
get_monotonic_time(),
|
get_monotonic_time(),
|
||||||
refresh,
|
Refresh::Unknown,
|
||||||
0,
|
0,
|
||||||
wp_presentation_feedback::Kind::empty(),
|
wp_presentation_feedback::Kind::empty(),
|
||||||
);
|
);
|
||||||
|
|||||||
+11
@@ -2,6 +2,7 @@ use std::ffi::OsString;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
use clap_complete::Shell;
|
||||||
use niri_ipc::{Action, OutputAction};
|
use niri_ipc::{Action, OutputAction};
|
||||||
|
|
||||||
use crate::utils::version;
|
use crate::utils::version;
|
||||||
@@ -54,6 +55,8 @@ pub enum Sub {
|
|||||||
},
|
},
|
||||||
/// Cause a panic to check if the backtraces are good.
|
/// Cause a panic to check if the backtraces are good.
|
||||||
Panic,
|
Panic,
|
||||||
|
/// Generate shell completions.
|
||||||
|
Completions { shell: Shell },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
@@ -64,12 +67,18 @@ pub enum Msg {
|
|||||||
Workspaces,
|
Workspaces,
|
||||||
/// List open windows.
|
/// List open windows.
|
||||||
Windows,
|
Windows,
|
||||||
|
/// List open layer-shell surfaces.
|
||||||
|
Layers,
|
||||||
/// Get the configured keyboard layouts.
|
/// Get the configured keyboard layouts.
|
||||||
KeyboardLayouts,
|
KeyboardLayouts,
|
||||||
/// Print information about the focused output.
|
/// Print information about the focused output.
|
||||||
FocusedOutput,
|
FocusedOutput,
|
||||||
/// Print information about the focused window.
|
/// Print information about the focused window.
|
||||||
FocusedWindow,
|
FocusedWindow,
|
||||||
|
/// Pick a window with the mouse and print information about it.
|
||||||
|
PickWindow,
|
||||||
|
/// Pick a color from the screen with the mouse.
|
||||||
|
PickColor,
|
||||||
/// Perform an action.
|
/// Perform an action.
|
||||||
Action {
|
Action {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
@@ -96,4 +105,6 @@ pub enum Msg {
|
|||||||
Version,
|
Version,
|
||||||
/// Request an error from the running niri instance.
|
/// Request an error from the running niri instance.
|
||||||
RequestError,
|
RequestError,
|
||||||
|
/// Print the overview state.
|
||||||
|
OverviewState,
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-15
@@ -4,12 +4,11 @@ use std::env;
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use smithay::backend::allocator::Fourcc;
|
use smithay::backend::allocator::Fourcc;
|
||||||
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
|
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
|
||||||
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
|
use smithay::input::pointer::{CursorIcon, CursorImageStatus, CursorImageSurfaceData};
|
||||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
|
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
|
||||||
use smithay::wayland::compositor::with_states;
|
use smithay::wayland::compositor::with_states;
|
||||||
@@ -67,7 +66,7 @@ impl CursorManager {
|
|||||||
let hotspot = with_states(&surface, |states| {
|
let hotspot = with_states(&surface, |states| {
|
||||||
states
|
states
|
||||||
.data_map
|
.data_map
|
||||||
.get::<Mutex<CursorImageAttributes>>()
|
.get::<CursorImageSurfaceData>()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -76,21 +75,24 @@ impl CursorManager {
|
|||||||
|
|
||||||
RenderCursor::Surface { hotspot, surface }
|
RenderCursor::Surface { hotspot, surface }
|
||||||
}
|
}
|
||||||
CursorImageStatus::Named(icon) => self
|
CursorImageStatus::Named(icon) => self.get_render_cursor_named(icon, scale),
|
||||||
.get_cursor_with_name(icon, scale)
|
|
||||||
.map(|cursor| RenderCursor::Named {
|
|
||||||
icon,
|
|
||||||
scale,
|
|
||||||
cursor,
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| RenderCursor::Named {
|
|
||||||
icon: Default::default(),
|
|
||||||
scale,
|
|
||||||
cursor: self.get_default_cursor(scale),
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_render_cursor_named(&self, icon: CursorIcon, scale: i32) -> RenderCursor {
|
||||||
|
self.get_cursor_with_name(icon, scale)
|
||||||
|
.map(|cursor| RenderCursor::Named {
|
||||||
|
icon,
|
||||||
|
scale,
|
||||||
|
cursor,
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| RenderCursor::Named {
|
||||||
|
icon: Default::default(),
|
||||||
|
scale,
|
||||||
|
cursor: self.get_default_cursor(scale),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_current_cursor_animated(&self, scale: i32) -> bool {
|
pub fn is_current_cursor_animated(&self, scale: i32) -> bool {
|
||||||
match &self.current_cursor {
|
match &self.current_cursor {
|
||||||
CursorImageStatus::Hidden => false,
|
CursorImageStatus::Hidden => false,
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ use std::sync::{Arc, Mutex, OnceLock};
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use zbus::fdo::{self, RequestNameFlags};
|
use zbus::fdo::{self, RequestNameFlags};
|
||||||
|
use zbus::message::Header;
|
||||||
use zbus::names::{OwnedUniqueName, UniqueName};
|
use zbus::names::{OwnedUniqueName, UniqueName};
|
||||||
use zbus::zvariant::NoneValue;
|
use zbus::zvariant::NoneValue;
|
||||||
use zbus::{dbus_interface, MessageHeader, Task};
|
use zbus::{interface, Task};
|
||||||
|
|
||||||
use super::Start;
|
use super::Start;
|
||||||
|
|
||||||
@@ -20,11 +21,11 @@ pub struct ScreenSaver {
|
|||||||
monitor_task: Arc<OnceLock<Task<()>>>,
|
monitor_task: Arc<OnceLock<Task<()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[dbus_interface(name = "org.freedesktop.ScreenSaver")]
|
#[interface(name = "org.freedesktop.ScreenSaver")]
|
||||||
impl ScreenSaver {
|
impl ScreenSaver {
|
||||||
async fn inhibit(
|
async fn inhibit(
|
||||||
&mut self,
|
&mut self,
|
||||||
#[zbus(header)] hdr: MessageHeader<'_>,
|
#[zbus(header)] hdr: Header<'_>,
|
||||||
application_name: &str,
|
application_name: &str,
|
||||||
reason_for_inhibit: &str,
|
reason_for_inhibit: &str,
|
||||||
) -> fdo::Result<u32> {
|
) -> fdo::Result<u32> {
|
||||||
@@ -33,7 +34,7 @@ impl ScreenSaver {
|
|||||||
hdr.sender()
|
hdr.sender()
|
||||||
);
|
);
|
||||||
|
|
||||||
let Ok(Some(name)) = hdr.sender() else {
|
let Some(name) = hdr.sender() else {
|
||||||
return Err(fdo::Error::Failed(String::from("no sender")));
|
return Err(fdo::Error::Failed(String::from("no sender")));
|
||||||
};
|
};
|
||||||
let name = OwnedUniqueName::from(name.to_owned());
|
let name = OwnedUniqueName::from(name.to_owned());
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use zbus::fdo::{self, RequestNameFlags};
|
use zbus::fdo::{self, RequestNameFlags};
|
||||||
|
use zbus::interface;
|
||||||
|
use zbus::object_server::SignalEmitter;
|
||||||
use zbus::zvariant::{SerializeDict, Type, Value};
|
use zbus::zvariant::{SerializeDict, Type, Value};
|
||||||
use zbus::{dbus_interface, SignalContext};
|
|
||||||
|
|
||||||
use super::Start;
|
use super::Start;
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ pub struct WindowProperties {
|
|||||||
pub app_id: String,
|
pub app_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[dbus_interface(name = "org.gnome.Shell.Introspect")]
|
#[interface(name = "org.gnome.Shell.Introspect")]
|
||||||
impl Introspect {
|
impl Introspect {
|
||||||
async fn get_windows(&self) -> fdo::Result<HashMap<u64, WindowProperties>> {
|
async fn get_windows(&self) -> fdo::Result<HashMap<u64, WindowProperties>> {
|
||||||
if let Err(err) = self.to_niri.send(IntrospectToNiri::GetWindows) {
|
if let Err(err) = self.to_niri.send(IntrospectToNiri::GetWindows) {
|
||||||
@@ -52,8 +53,8 @@ impl Introspect {
|
|||||||
|
|
||||||
// FIXME: call this upon window changes, once more of the infrastructure is there (will be
|
// FIXME: call this upon window changes, once more of the infrastructure is there (will be
|
||||||
// needed for the event stream IPC anyway).
|
// needed for the event stream IPC anyway).
|
||||||
#[dbus_interface(signal)]
|
#[zbus(signal)]
|
||||||
pub async fn windows_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
|
pub async fn windows_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Introspect {
|
impl Introspect {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use zbus::dbus_interface;
|
use niri_ipc::PickedColor;
|
||||||
use zbus::fdo::{self, RequestNameFlags};
|
use zbus::fdo::{self, RequestNameFlags};
|
||||||
|
use zbus::zvariant::OwnedValue;
|
||||||
|
use zbus::{interface, zvariant};
|
||||||
|
|
||||||
use super::Start;
|
use super::Start;
|
||||||
|
|
||||||
@@ -12,13 +15,14 @@ pub struct Screenshot {
|
|||||||
|
|
||||||
pub enum ScreenshotToNiri {
|
pub enum ScreenshotToNiri {
|
||||||
TakeScreenshot { include_cursor: bool },
|
TakeScreenshot { include_cursor: bool },
|
||||||
|
PickColor(async_channel::Sender<Option<PickedColor>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum NiriToScreenshot {
|
pub enum NiriToScreenshot {
|
||||||
ScreenshotResult(Option<PathBuf>),
|
ScreenshotResult(Option<PathBuf>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[dbus_interface(name = "org.gnome.Shell.Screenshot")]
|
#[interface(name = "org.gnome.Shell.Screenshot")]
|
||||||
impl Screenshot {
|
impl Screenshot {
|
||||||
async fn screenshot(
|
async fn screenshot(
|
||||||
&self,
|
&self,
|
||||||
@@ -47,6 +51,34 @@ impl Screenshot {
|
|||||||
|
|
||||||
Ok((true, filename))
|
Ok((true, filename))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn pick_color(&self) -> fdo::Result<HashMap<String, OwnedValue>> {
|
||||||
|
let (tx, rx) = async_channel::bounded(1);
|
||||||
|
if let Err(err) = self.to_niri.send(ScreenshotToNiri::PickColor(tx)) {
|
||||||
|
warn!("error sending pick color message to niri: {err:?}");
|
||||||
|
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let color = match rx.recv().await {
|
||||||
|
Ok(Some(color)) => color,
|
||||||
|
Ok(None) => {
|
||||||
|
return Err(fdo::Error::Failed("no color picked".to_owned()));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error receiving message from niri: {err:?}");
|
||||||
|
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut result = HashMap::new();
|
||||||
|
let [r, g, b] = color.rgb;
|
||||||
|
result.insert(
|
||||||
|
"color".to_string(),
|
||||||
|
zvariant::OwnedValue::try_from(zvariant::Value::from((r, g, b))).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Screenshot {
|
impl Screenshot {
|
||||||
|
|||||||
+31
-6
@@ -1,5 +1,5 @@
|
|||||||
use zbus::blocking::Connection;
|
use zbus::blocking::Connection;
|
||||||
use zbus::Interface;
|
use zbus::object_server::Interface;
|
||||||
|
|
||||||
use crate::niri::State;
|
use crate::niri::State;
|
||||||
|
|
||||||
@@ -45,12 +45,39 @@ impl DBusServers {
|
|||||||
let mut dbus = Self::default();
|
let mut dbus = Self::default();
|
||||||
|
|
||||||
if is_session_instance {
|
if is_session_instance {
|
||||||
let service_channel = ServiceChannel::new(niri.display_handle.clone());
|
let (to_niri, from_service_channel) = calloop::channel::channel();
|
||||||
|
let service_channel = ServiceChannel::new(to_niri);
|
||||||
|
niri.event_loop
|
||||||
|
.insert_source(from_service_channel, move |event, _, state| match event {
|
||||||
|
calloop::channel::Event::Msg(new_client) => {
|
||||||
|
state.niri.insert_client(new_client);
|
||||||
|
}
|
||||||
|
calloop::channel::Event::Closed => (),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
dbus.conn_service_channel = try_start(service_channel);
|
dbus.conn_service_channel = try_start(service_channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
|
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
|
||||||
let display_config = DisplayConfig::new(backend.ipc_outputs());
|
let (to_niri, from_display_config) = calloop::channel::channel();
|
||||||
|
let display_config = DisplayConfig::new(to_niri, backend.ipc_outputs());
|
||||||
|
niri.event_loop
|
||||||
|
.insert_source(from_display_config, move |event, _, state| match event {
|
||||||
|
calloop::channel::Event::Msg(new_conf) => {
|
||||||
|
for (name, conf) in new_conf {
|
||||||
|
state.modify_output_config(&name, move |output| {
|
||||||
|
if let Some(new_output) = conf {
|
||||||
|
*output = new_output;
|
||||||
|
} else {
|
||||||
|
output.off = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
state.reload_output_config();
|
||||||
|
}
|
||||||
|
calloop::channel::Event::Closed => (),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
dbus.conn_display_config = try_start(display_config);
|
dbus.conn_display_config = try_start(display_config);
|
||||||
|
|
||||||
let screen_saver = ScreenSaver::new(niri.is_fdo_idle_inhibited.clone());
|
let screen_saver = ScreenSaver::new(niri.is_fdo_idle_inhibited.clone());
|
||||||
@@ -83,7 +110,7 @@ impl DBusServers {
|
|||||||
dbus.conn_introspect = try_start(introspect);
|
dbus.conn_introspect = try_start(introspect);
|
||||||
|
|
||||||
#[cfg(feature = "xdp-gnome-screencast")]
|
#[cfg(feature = "xdp-gnome-screencast")]
|
||||||
if niri.pipewire.is_some() {
|
{
|
||||||
let (to_niri, from_screen_cast) = calloop::channel::channel();
|
let (to_niri, from_screen_cast) = calloop::channel::channel();
|
||||||
niri.event_loop
|
niri.event_loop
|
||||||
.insert_source(from_screen_cast, {
|
.insert_source(from_screen_cast, {
|
||||||
@@ -95,8 +122,6 @@ impl DBusServers {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let screen_cast = ScreenCast::new(backend.ipc_outputs(), to_niri);
|
let screen_cast = ScreenCast::new(backend.ipc_outputs(), to_niri);
|
||||||
dbus.conn_screen_cast = try_start(screen_cast);
|
dbus.conn_screen_cast = try_start(screen_cast);
|
||||||
} else {
|
|
||||||
warn!("disabling screencast support because we couldn't start PipeWire");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
use smithay::utils::Size;
|
||||||
use zbus::fdo::RequestNameFlags;
|
use zbus::fdo::RequestNameFlags;
|
||||||
|
use zbus::object_server::SignalEmitter;
|
||||||
use zbus::zvariant::{self, OwnedValue, Type};
|
use zbus::zvariant::{self, OwnedValue, Type};
|
||||||
use zbus::{dbus_interface, fdo, SignalContext};
|
use zbus::{fdo, interface};
|
||||||
|
|
||||||
use super::Start;
|
use super::Start;
|
||||||
use crate::backend::IpcOutputMap;
|
use crate::backend::IpcOutputMap;
|
||||||
use crate::utils::is_laptop_panel;
|
use crate::utils::is_laptop_panel;
|
||||||
|
use crate::utils::scale::supported_scales;
|
||||||
|
|
||||||
pub struct DisplayConfig {
|
pub struct DisplayConfig {
|
||||||
|
to_niri: calloop::channel::Sender<HashMap<String, Option<niri_config::Output>>>,
|
||||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +48,18 @@ pub struct LogicalMonitor {
|
|||||||
properties: HashMap<String, OwnedValue>,
|
properties: HashMap<String, OwnedValue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[dbus_interface(name = "org.gnome.Mutter.DisplayConfig")]
|
// ApplyMonitorsConfig
|
||||||
|
#[derive(Deserialize, Type)]
|
||||||
|
pub struct LogicalMonitorConfiguration {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
scale: f64,
|
||||||
|
transform: u32,
|
||||||
|
_is_primary: bool,
|
||||||
|
monitors: Vec<(String, String, HashMap<String, OwnedValue>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[interface(name = "org.gnome.Mutter.DisplayConfig")]
|
||||||
impl DisplayConfig {
|
impl DisplayConfig {
|
||||||
async fn get_current_state(
|
async fn get_current_state(
|
||||||
&self,
|
&self,
|
||||||
@@ -54,75 +70,70 @@ impl DisplayConfig {
|
|||||||
HashMap<String, OwnedValue>,
|
HashMap<String, OwnedValue>,
|
||||||
)> {
|
)> {
|
||||||
// Construct the DBus response.
|
// Construct the DBus response.
|
||||||
let mut monitors: Vec<(Monitor, LogicalMonitor)> = self
|
let mut monitors = Vec::new();
|
||||||
.ipc_outputs
|
let mut logical_monitors = Vec::new();
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.values()
|
|
||||||
// Take only enabled outputs.
|
|
||||||
.filter(|output| output.current_mode.is_some() && output.logical.is_some())
|
|
||||||
.map(|output| {
|
|
||||||
// Loosely matches the check in Mutter.
|
|
||||||
let c = &output.name;
|
|
||||||
let is_laptop_panel = is_laptop_panel(c);
|
|
||||||
let display_name = make_display_name(output, is_laptop_panel);
|
|
||||||
|
|
||||||
let mut properties = HashMap::new();
|
for output in self.ipc_outputs.lock().unwrap().values() {
|
||||||
properties.insert(
|
// Loosely matches the check in Mutter.
|
||||||
String::from("display-name"),
|
let c = &output.name;
|
||||||
OwnedValue::from(zvariant::Str::from(display_name)),
|
let is_laptop_panel = is_laptop_panel(c);
|
||||||
);
|
let display_name = make_display_name(output, is_laptop_panel);
|
||||||
properties.insert(
|
|
||||||
String::from("is-builtin"),
|
|
||||||
OwnedValue::from(is_laptop_panel),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut modes: Vec<Mode> = output
|
let mut properties = HashMap::new();
|
||||||
.modes
|
properties.insert(
|
||||||
.iter()
|
String::from("display-name"),
|
||||||
.map(|m| {
|
OwnedValue::from(zvariant::Str::from(display_name)),
|
||||||
let niri_ipc::Mode {
|
);
|
||||||
width,
|
properties.insert(
|
||||||
height,
|
String::from("is-builtin"),
|
||||||
refresh_rate,
|
OwnedValue::from(is_laptop_panel),
|
||||||
is_preferred,
|
);
|
||||||
} = *m;
|
|
||||||
let refresh = refresh_rate as f64 / 1000.;
|
|
||||||
|
|
||||||
Mode {
|
let mut modes: Vec<Mode> = output
|
||||||
id: format!("{width}x{height}@{refresh:.3}"),
|
.modes
|
||||||
width: i32::from(width),
|
.iter()
|
||||||
height: i32::from(height),
|
.map(|m| {
|
||||||
refresh_rate: refresh,
|
let niri_ipc::Mode {
|
||||||
preferred_scale: 1.,
|
width,
|
||||||
supported_scales: vec![1., 2., 3.],
|
height,
|
||||||
properties: HashMap::from([(
|
refresh_rate,
|
||||||
String::from("is-preferred"),
|
is_preferred,
|
||||||
OwnedValue::from(is_preferred),
|
} = *m;
|
||||||
)]),
|
let width = i32::from(width);
|
||||||
}
|
let height = i32::from(height);
|
||||||
})
|
let refresh_rate = refresh_rate as f64 / 1000.;
|
||||||
.collect();
|
|
||||||
modes[output.current_mode.unwrap()]
|
Mode {
|
||||||
|
id: format!("{width}x{height}@{refresh_rate:.3}"),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
refresh_rate,
|
||||||
|
preferred_scale: 1.,
|
||||||
|
supported_scales: supported_scales(Size::from((width, height))).collect(),
|
||||||
|
properties: HashMap::from([(
|
||||||
|
String::from("is-preferred"),
|
||||||
|
OwnedValue::from(is_preferred),
|
||||||
|
)]),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if let Some(mode) = output.current_mode {
|
||||||
|
modes[mode]
|
||||||
.properties
|
.properties
|
||||||
.insert(String::from("is-current"), OwnedValue::from(true));
|
.insert(String::from("is-current"), OwnedValue::from(true));
|
||||||
|
}
|
||||||
|
|
||||||
let connector = c.clone();
|
let connector = c.clone();
|
||||||
let model = output.model.clone();
|
let model = output.model.clone();
|
||||||
let make = output.make.clone();
|
let make = output.make.clone();
|
||||||
|
|
||||||
// Serial is used for session restore, so fall back to the connector name if it's
|
// Serial is used for session restore, so fall back to the connector name if it's
|
||||||
// not available.
|
// not available.
|
||||||
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
|
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
|
||||||
|
|
||||||
let monitor = Monitor {
|
let names = (connector, make, model, serial);
|
||||||
names: (connector, make, model, serial),
|
|
||||||
modes,
|
|
||||||
properties,
|
|
||||||
};
|
|
||||||
|
|
||||||
let logical = output.logical.as_ref().unwrap();
|
|
||||||
|
|
||||||
|
if let Some(logical) = output.logical.as_ref() {
|
||||||
let transform = match logical.transform {
|
let transform = match logical.transform {
|
||||||
niri_ipc::Transform::Normal => 0,
|
niri_ipc::Transform::Normal => 0,
|
||||||
niri_ipc::Transform::_90 => 1,
|
niri_ipc::Transform::_90 => 1,
|
||||||
@@ -134,35 +145,151 @@ impl DisplayConfig {
|
|||||||
niri_ipc::Transform::Flipped270 => 7,
|
niri_ipc::Transform::Flipped270 => 7,
|
||||||
};
|
};
|
||||||
|
|
||||||
let logical_monitor = LogicalMonitor {
|
logical_monitors.push(LogicalMonitor {
|
||||||
x: logical.x,
|
x: logical.x,
|
||||||
y: logical.y,
|
y: logical.y,
|
||||||
scale: logical.scale,
|
scale: logical.scale,
|
||||||
transform,
|
transform,
|
||||||
is_primary: false,
|
is_primary: false,
|
||||||
monitors: vec![monitor.names.clone()],
|
monitors: vec![names.clone()],
|
||||||
properties: HashMap::new(),
|
properties: HashMap::new(),
|
||||||
};
|
});
|
||||||
|
}
|
||||||
|
|
||||||
(monitor, logical_monitor)
|
monitors.push(Monitor {
|
||||||
})
|
names,
|
||||||
.collect();
|
modes,
|
||||||
|
properties,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Sort by connector.
|
// Sort by connector.
|
||||||
monitors.sort_unstable_by(|a, b| a.0.names.0.cmp(&b.0.names.0));
|
monitors.sort_unstable_by(|a, b| a.names.0.cmp(&b.names.0));
|
||||||
|
logical_monitors.sort_unstable_by(|a, b| a.monitors[0].0.cmp(&b.monitors[0].0));
|
||||||
|
|
||||||
let (monitors, logical_monitors) = monitors.into_iter().unzip();
|
|
||||||
let properties = HashMap::from([(String::from("layout-mode"), OwnedValue::from(1u32))]);
|
let properties = HashMap::from([(String::from("layout-mode"), OwnedValue::from(1u32))]);
|
||||||
Ok((0, monitors, logical_monitors, properties))
|
Ok((0, monitors, logical_monitors, properties))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[dbus_interface(signal)]
|
async fn apply_monitors_config(
|
||||||
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
|
&self,
|
||||||
|
_serial: u32,
|
||||||
|
method: u32,
|
||||||
|
logical_monitor_configs: Vec<LogicalMonitorConfiguration>,
|
||||||
|
_properties: HashMap<String, OwnedValue>,
|
||||||
|
) -> fdo::Result<()> {
|
||||||
|
let current_conf = self.ipc_outputs.lock().unwrap();
|
||||||
|
let mut new_conf = HashMap::new();
|
||||||
|
for requested_config in logical_monitor_configs {
|
||||||
|
if requested_config.monitors.len() > 1 {
|
||||||
|
return Err(zbus::fdo::Error::Failed(
|
||||||
|
"Mirroring is not yet supported".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for (connector, mode, _props) in requested_config.monitors {
|
||||||
|
if !current_conf.values().any(|o| o.name == connector) {
|
||||||
|
return Err(zbus::fdo::Error::Failed(format!(
|
||||||
|
"Connector '{}' not found",
|
||||||
|
connector
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
new_conf.insert(
|
||||||
|
connector.clone(),
|
||||||
|
Some(niri_config::Output {
|
||||||
|
off: false,
|
||||||
|
name: connector,
|
||||||
|
scale: Some(niri_config::FloatOrInt(requested_config.scale)),
|
||||||
|
transform: match requested_config.transform {
|
||||||
|
0 => niri_ipc::Transform::Normal,
|
||||||
|
1 => niri_ipc::Transform::_90,
|
||||||
|
2 => niri_ipc::Transform::_180,
|
||||||
|
3 => niri_ipc::Transform::_270,
|
||||||
|
4 => niri_ipc::Transform::Flipped,
|
||||||
|
5 => niri_ipc::Transform::Flipped90,
|
||||||
|
6 => niri_ipc::Transform::Flipped180,
|
||||||
|
7 => niri_ipc::Transform::Flipped270,
|
||||||
|
x => {
|
||||||
|
return Err(zbus::fdo::Error::Failed(format!(
|
||||||
|
"Unknown transform {}",
|
||||||
|
x
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
position: Some(niri_config::Position {
|
||||||
|
x: requested_config.x,
|
||||||
|
y: requested_config.y,
|
||||||
|
}),
|
||||||
|
mode: Some(niri_ipc::ConfiguredMode::from_str(&mode).map_err(|e| {
|
||||||
|
zbus::fdo::Error::Failed(format!(
|
||||||
|
"Could not parse mode '{}': {}",
|
||||||
|
mode, e
|
||||||
|
))
|
||||||
|
})?),
|
||||||
|
// FIXME: VRR
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if new_conf.is_empty() {
|
||||||
|
return Err(zbus::fdo::Error::Failed(
|
||||||
|
"At least one output must be enabled".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for output in current_conf.values() {
|
||||||
|
if !new_conf.contains_key(&output.name) {
|
||||||
|
new_conf.insert(output.name.clone(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if method == 0 {
|
||||||
|
// 0 means "verify", so don't actually apply here
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if let Err(err) = self.to_niri.send(new_conf) {
|
||||||
|
warn!("error sending message to niri: {err:?}");
|
||||||
|
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(signal)]
|
||||||
|
pub async fn monitors_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn power_save_mode(&self) -> i32 {
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn set_power_save_mode(&self, _mode: i32) -> zbus::Result<()> {
|
||||||
|
Err(zbus::Error::Unsupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn panel_orientation_managed(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn apply_monitors_config_allowed(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn night_light_supported(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayConfig {
|
impl DisplayConfig {
|
||||||
pub fn new(ipc_outputs: Arc<Mutex<IpcOutputMap>>) -> Self {
|
pub fn new(
|
||||||
Self { ipc_outputs }
|
to_niri: calloop::channel::Sender<HashMap<String, Option<niri_config::Output>>>,
|
||||||
|
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
to_niri,
|
||||||
|
ipc_outputs,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,16 +339,16 @@ fn format_diagonal(diagonal_inches: f64) -> String {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use k9::snapshot;
|
use insta::assert_snapshot;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_diagonal() {
|
fn test_format_diagonal() {
|
||||||
snapshot!(format_diagonal(12.11), "12.1″");
|
assert_snapshot!(format_diagonal(12.11), @"12.1″");
|
||||||
snapshot!(format_diagonal(13.28), "13.3″");
|
assert_snapshot!(format_diagonal(13.28), @"13.3″");
|
||||||
snapshot!(format_diagonal(15.6), "15.6″");
|
assert_snapshot!(format_diagonal(15.6), @"15.6″");
|
||||||
snapshot!(format_diagonal(23.2), "23″");
|
assert_snapshot!(format_diagonal(23.2), @"23″");
|
||||||
snapshot!(format_diagonal(24.8), "25″");
|
assert_snapshot!(format_diagonal(24.8), @"25″");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ use std::sync::{Arc, Mutex};
|
|||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use zbus::fdo::RequestNameFlags;
|
use zbus::fdo::RequestNameFlags;
|
||||||
|
use zbus::object_server::{InterfaceRef, SignalEmitter};
|
||||||
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
|
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 super::Start;
|
||||||
use crate::backend::IpcOutputMap;
|
use crate::backend::IpcOutputMap;
|
||||||
@@ -61,6 +62,8 @@ static STREAM_ID: AtomicUsize = AtomicUsize::new(0);
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Stream {
|
pub struct Stream {
|
||||||
|
id: usize,
|
||||||
|
session_id: usize,
|
||||||
target: StreamTarget,
|
target: StreamTarget,
|
||||||
cursor_mode: CursorMode,
|
cursor_mode: CursorMode,
|
||||||
was_started: Arc<AtomicBool>,
|
was_started: Arc<AtomicBool>,
|
||||||
@@ -92,16 +95,17 @@ struct StreamParameters {
|
|||||||
pub enum ScreenCastToNiri {
|
pub enum ScreenCastToNiri {
|
||||||
StartCast {
|
StartCast {
|
||||||
session_id: usize,
|
session_id: usize,
|
||||||
|
stream_id: usize,
|
||||||
target: StreamTargetId,
|
target: StreamTargetId,
|
||||||
cursor_mode: CursorMode,
|
cursor_mode: CursorMode,
|
||||||
signal_ctx: SignalContext<'static>,
|
signal_ctx: SignalEmitter<'static>,
|
||||||
},
|
},
|
||||||
StopCast {
|
StopCast {
|
||||||
session_id: usize,
|
session_id: usize,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
|
#[interface(name = "org.gnome.Mutter.ScreenCast")]
|
||||||
impl ScreenCast {
|
impl ScreenCast {
|
||||||
async fn create_session(
|
async fn create_session(
|
||||||
&self,
|
&self,
|
||||||
@@ -136,26 +140,26 @@ impl ScreenCast {
|
|||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[dbus_interface(property)]
|
#[zbus(property)]
|
||||||
async fn version(&self) -> i32 {
|
async fn version(&self) -> i32 {
|
||||||
4
|
4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Session")]
|
#[interface(name = "org.gnome.Mutter.ScreenCast.Session")]
|
||||||
impl Session {
|
impl Session {
|
||||||
async fn start(&self) {
|
async fn start(&self) {
|
||||||
debug!("start");
|
debug!("start");
|
||||||
|
|
||||||
for (stream, iface) in &*self.streams.lock().unwrap() {
|
for (stream, iface) in &*self.streams.lock().unwrap() {
|
||||||
stream.start(self.id, iface.signal_context().clone());
|
stream.start(iface.signal_emitter().clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stop(
|
pub async fn stop(
|
||||||
&self,
|
&self,
|
||||||
#[zbus(object_server)] server: &ObjectServer,
|
#[zbus(object_server)] server: &ObjectServer,
|
||||||
#[zbus(signal_context)] ctxt: SignalContext<'_>,
|
#[zbus(signal_context)] ctxt: SignalEmitter<'_>,
|
||||||
) {
|
) {
|
||||||
debug!("stop");
|
debug!("stop");
|
||||||
|
|
||||||
@@ -175,7 +179,7 @@ impl Session {
|
|||||||
let streams = mem::take(&mut *self.streams.lock().unwrap());
|
let streams = mem::take(&mut *self.streams.lock().unwrap());
|
||||||
for (_, iface) in streams.iter() {
|
for (_, iface) in streams.iter() {
|
||||||
server
|
server
|
||||||
.remove::<Stream, _>(iface.signal_context().path())
|
.remove::<Stream, _>(iface.signal_emitter().path())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
@@ -203,16 +207,20 @@ impl Session {
|
|||||||
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
|
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = format!(
|
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
|
||||||
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
|
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
|
||||||
STREAM_ID.fetch_add(1, Ordering::SeqCst)
|
|
||||||
);
|
|
||||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||||
|
|
||||||
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
||||||
|
|
||||||
let target = StreamTarget::Output(output);
|
let target = StreamTarget::Output(output);
|
||||||
let stream = Stream::new(target, cursor_mode, self.to_niri.clone());
|
let stream = Stream::new(
|
||||||
|
stream_id,
|
||||||
|
self.id,
|
||||||
|
target,
|
||||||
|
cursor_mode,
|
||||||
|
self.to_niri.clone(),
|
||||||
|
);
|
||||||
match server.at(&path, stream.clone()).await {
|
match server.at(&path, stream.clone()).await {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
let iface = server.interface(&path).await.unwrap();
|
let iface = server.interface(&path).await.unwrap();
|
||||||
@@ -236,10 +244,8 @@ impl Session {
|
|||||||
) -> fdo::Result<OwnedObjectPath> {
|
) -> fdo::Result<OwnedObjectPath> {
|
||||||
debug!(?properties, "record_window");
|
debug!(?properties, "record_window");
|
||||||
|
|
||||||
let path = format!(
|
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
|
||||||
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
|
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
|
||||||
STREAM_ID.fetch_add(1, Ordering::SeqCst)
|
|
||||||
);
|
|
||||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||||
|
|
||||||
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
||||||
@@ -247,7 +253,13 @@ impl Session {
|
|||||||
let target = StreamTarget::Window {
|
let target = StreamTarget::Window {
|
||||||
id: properties.window_id,
|
id: properties.window_id,
|
||||||
};
|
};
|
||||||
let stream = Stream::new(target, cursor_mode, self.to_niri.clone());
|
let stream = Stream::new(
|
||||||
|
stream_id,
|
||||||
|
self.id,
|
||||||
|
target,
|
||||||
|
cursor_mode,
|
||||||
|
self.to_niri.clone(),
|
||||||
|
);
|
||||||
match server.at(&path, stream.clone()).await {
|
match server.at(&path, stream.clone()).await {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
let iface = server.interface(&path).await.unwrap();
|
let iface = server.interface(&path).await.unwrap();
|
||||||
@@ -264,17 +276,17 @@ impl Session {
|
|||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[dbus_interface(signal)]
|
#[zbus(signal)]
|
||||||
async fn closed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
|
async fn closed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Stream")]
|
#[interface(name = "org.gnome.Mutter.ScreenCast.Stream")]
|
||||||
impl Stream {
|
impl Stream {
|
||||||
#[dbus_interface(signal)]
|
#[zbus(signal)]
|
||||||
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
|
pub async fn pipe_wire_stream_added(ctxt: &SignalEmitter<'_>, node_id: u32)
|
||||||
-> zbus::Result<()>;
|
-> zbus::Result<()>;
|
||||||
|
|
||||||
#[dbus_interface(property)]
|
#[zbus(property)]
|
||||||
async fn parameters(&self) -> StreamParameters {
|
async fn parameters(&self) -> StreamParameters {
|
||||||
match &self.target {
|
match &self.target {
|
||||||
StreamTarget::Output(output) => {
|
StreamTarget::Output(output) => {
|
||||||
@@ -349,11 +361,15 @@ impl Drop for Session {
|
|||||||
|
|
||||||
impl Stream {
|
impl Stream {
|
||||||
fn new(
|
fn new(
|
||||||
|
id: usize,
|
||||||
|
session_id: usize,
|
||||||
target: StreamTarget,
|
target: StreamTarget,
|
||||||
cursor_mode: CursorMode,
|
cursor_mode: CursorMode,
|
||||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
id,
|
||||||
|
session_id,
|
||||||
target,
|
target,
|
||||||
cursor_mode,
|
cursor_mode,
|
||||||
was_started: Arc::new(AtomicBool::new(false)),
|
was_started: Arc::new(AtomicBool::new(false)),
|
||||||
@@ -361,13 +377,14 @@ impl Stream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start(&self, session_id: usize, ctxt: SignalContext<'static>) {
|
fn start(&self, ctxt: SignalEmitter<'static>) {
|
||||||
if self.was_started.load(Ordering::SeqCst) {
|
if self.was_started.load(Ordering::SeqCst) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = ScreenCastToNiri::StartCast {
|
let msg = ScreenCastToNiri::StartCast {
|
||||||
session_id,
|
session_id: self.session_id,
|
||||||
|
stream_id: self.id,
|
||||||
target: self.target.make_id(),
|
target: self.target.make_id(),
|
||||||
cursor_mode: self.cursor_mode,
|
cursor_mode: self.cursor_mode,
|
||||||
signal_ctx: ctxt,
|
signal_ctx: ctxt,
|
||||||
|
|||||||
@@ -1,50 +1,51 @@
|
|||||||
use std::os::fd::{FromRawFd, IntoRawFd};
|
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use smithay::reexports::wayland_server::DisplayHandle;
|
use zbus::{fdo, interface, zvariant};
|
||||||
use zbus::dbus_interface;
|
|
||||||
|
|
||||||
use super::Start;
|
use super::Start;
|
||||||
use crate::niri::ClientState;
|
use crate::niri::NewClient;
|
||||||
|
|
||||||
pub struct ServiceChannel {
|
pub struct ServiceChannel {
|
||||||
display: DisplayHandle,
|
to_niri: calloop::channel::Sender<NewClient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[dbus_interface(name = "org.gnome.Mutter.ServiceChannel")]
|
#[interface(name = "org.gnome.Mutter.ServiceChannel")]
|
||||||
impl ServiceChannel {
|
impl ServiceChannel {
|
||||||
async fn open_wayland_service_connection(
|
async fn open_wayland_service_connection(
|
||||||
&mut self,
|
&mut self,
|
||||||
service_client_type: u32,
|
service_client_type: u32,
|
||||||
) -> zbus::fdo::Result<zbus::zvariant::OwnedFd> {
|
) -> fdo::Result<zvariant::OwnedFd> {
|
||||||
if service_client_type != 1 {
|
if service_client_type != 1 {
|
||||||
return Err(zbus::fdo::Error::InvalidArgs(
|
return Err(fdo::Error::InvalidArgs(
|
||||||
"Invalid service client type".to_owned(),
|
"Invalid service client type".to_owned(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (sock1, sock2) = UnixStream::pair().unwrap();
|
let (sock1, sock2) = UnixStream::pair().unwrap();
|
||||||
let data = Arc::new(ClientState {
|
let client = NewClient {
|
||||||
compositor_state: Default::default(),
|
client: sock2,
|
||||||
// Would be nice to thread config here but for now it's fine.
|
|
||||||
can_view_decoration_globals: false,
|
|
||||||
restricted: false,
|
restricted: false,
|
||||||
});
|
// FIXME: maybe you can get the PID from D-Bus somehow?
|
||||||
self.display.insert_client(sock2, data).unwrap();
|
credentials_unknown: true,
|
||||||
Ok(unsafe { zbus::zvariant::OwnedFd::from_raw_fd(sock1.into_raw_fd()) })
|
};
|
||||||
|
if let Err(err) = self.to_niri.send(client) {
|
||||||
|
warn!("error sending message to niri: {err:?}");
|
||||||
|
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(zvariant::OwnedFd::from(std::os::fd::OwnedFd::from(sock1)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServiceChannel {
|
impl ServiceChannel {
|
||||||
pub fn new(display: DisplayHandle) -> Self {
|
pub fn new(to_niri: calloop::channel::Sender<NewClient>) -> Self {
|
||||||
Self { display }
|
Self { to_niri }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Start for ServiceChannel {
|
impl Start for ServiceChannel {
|
||||||
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
|
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")?
|
.name("org.gnome.Mutter.ServiceChannel")?
|
||||||
.serve_at("/org/gnome/Mutter/ServiceChannel", self)?
|
.serve_at("/org/gnome/Mutter/ServiceChannel", self)?
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|||||||
+126
-64
@@ -1,6 +1,7 @@
|
|||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
|
|
||||||
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
|
use niri_ipc::PositionChange;
|
||||||
|
use smithay::backend::renderer::utils::on_commit_buffer_handler;
|
||||||
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
|
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
|
||||||
use smithay::reexports::calloop::Interest;
|
use smithay::reexports::calloop::Interest;
|
||||||
use smithay::reexports::wayland_server::protocol::wl_buffer;
|
use smithay::reexports::wayland_server::protocol::wl_buffer;
|
||||||
@@ -18,9 +19,11 @@ use smithay::wayland::shm::{ShmHandler, ShmState};
|
|||||||
use smithay::{delegate_compositor, delegate_shm};
|
use smithay::{delegate_compositor, delegate_shm};
|
||||||
|
|
||||||
use super::xdg_shell::add_mapped_toplevel_pre_commit_hook;
|
use super::xdg_shell::add_mapped_toplevel_pre_commit_hook;
|
||||||
use crate::niri::{ClientState, State};
|
use crate::handlers::XDG_ACTIVATION_TOKEN_TIMEOUT;
|
||||||
use crate::utils::send_scale_transform;
|
use crate::layout::{ActivateWindow, AddWindowTarget};
|
||||||
|
use crate::niri::{CastTarget, ClientState, LockState, State};
|
||||||
use crate::utils::transaction::Transaction;
|
use crate::utils::transaction::Transaction;
|
||||||
|
use crate::utils::{is_mapped, send_scale_transform};
|
||||||
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
|
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
|
||||||
|
|
||||||
impl CompositorHandler for State {
|
impl CompositorHandler for State {
|
||||||
@@ -75,25 +78,25 @@ impl CompositorHandler for State {
|
|||||||
if surface == &root_surface {
|
if surface == &root_surface {
|
||||||
// This is a root surface commit. It might have mapped a previously-unmapped toplevel.
|
// 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()) {
|
if let Entry::Occupied(entry) = self.niri.unmapped_windows.entry(surface.clone()) {
|
||||||
let is_mapped =
|
if is_mapped(surface) {
|
||||||
with_renderer_surface_state(surface, |state| state.buffer().is_some())
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
error!("no renderer surface state even though we use commit handler");
|
|
||||||
false
|
|
||||||
});
|
|
||||||
|
|
||||||
if is_mapped {
|
|
||||||
// The toplevel got mapped.
|
// The toplevel got mapped.
|
||||||
let Unmapped { window, state } = entry.remove();
|
let Unmapped {
|
||||||
|
window,
|
||||||
|
state,
|
||||||
|
activation_token_data,
|
||||||
|
} = entry.remove();
|
||||||
|
|
||||||
window.on_commit();
|
window.on_commit();
|
||||||
|
|
||||||
let toplevel = window.toplevel().expect("no X11 support");
|
let toplevel = window.toplevel().expect("no X11 support");
|
||||||
|
|
||||||
let (rules, width, is_full_width, output, workspace_name) =
|
let (rules, width, height, is_full_width, output, workspace_id) =
|
||||||
if let InitialConfigureState::Configured {
|
if let InitialConfigureState::Configured {
|
||||||
rules,
|
rules,
|
||||||
width,
|
width,
|
||||||
|
height,
|
||||||
|
floating_width: _,
|
||||||
|
floating_height: _,
|
||||||
is_full_width,
|
is_full_width,
|
||||||
output,
|
output,
|
||||||
workspace_name,
|
workspace_name,
|
||||||
@@ -104,15 +107,48 @@ impl CompositorHandler for State {
|
|||||||
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
|
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
|
||||||
|
|
||||||
// Check that the workspace still exists.
|
// Check that the workspace still exists.
|
||||||
let workspace_name = workspace_name
|
let workspace_id = workspace_name
|
||||||
.filter(|n| self.niri.layout.find_workspace_by_name(n).is_some());
|
.as_deref()
|
||||||
|
.and_then(|n| self.niri.layout.find_workspace_by_name(n))
|
||||||
|
.map(|(_, ws)| ws.id());
|
||||||
|
|
||||||
(rules, width, is_full_width, output, workspace_name)
|
(rules, width, height, is_full_width, output, workspace_id)
|
||||||
} else {
|
} else {
|
||||||
error!("window map must happen after initial configure");
|
error!("window map must happen after initial configure");
|
||||||
(ResolvedWindowRules::empty(), None, false, None, None)
|
(ResolvedWindowRules::empty(), None, None, false, None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The GTK about dialog sets min/max size after the initial configure but
|
||||||
|
// before mapping, so we need to compute open_floating at the last possible
|
||||||
|
// moment, that is here.
|
||||||
|
let is_floating = rules.compute_open_floating(toplevel);
|
||||||
|
|
||||||
|
// Figure out if we should activate the window.
|
||||||
|
let activate = rules.open_focused.map(|focus| {
|
||||||
|
if focus {
|
||||||
|
ActivateWindow::Yes
|
||||||
|
} else {
|
||||||
|
ActivateWindow::No
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let activate = activate.unwrap_or_else(|| {
|
||||||
|
// Check the token timestamp again in case the window took a while between
|
||||||
|
// requesting activation and mapping.
|
||||||
|
let token = activation_token_data.filter(|token| {
|
||||||
|
token.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT
|
||||||
|
});
|
||||||
|
if token.is_some() {
|
||||||
|
ActivateWindow::Yes
|
||||||
|
} else {
|
||||||
|
let config = self.niri.config.borrow();
|
||||||
|
if config.debug.strict_new_window_focus_policy {
|
||||||
|
ActivateWindow::No
|
||||||
|
} else {
|
||||||
|
ActivateWindow::Smart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let parent = toplevel
|
let parent = toplevel
|
||||||
.parent()
|
.parent()
|
||||||
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||||
@@ -123,7 +159,9 @@ impl CompositorHandler for State {
|
|||||||
// None. If the configured output is set, that means it was set explicitly
|
// None. If the configured output is set, that means it was set explicitly
|
||||||
// by a window rule or a fullscreen request.
|
// by a window rule or a fullscreen request.
|
||||||
.filter(|(_, parent_output)| {
|
.filter(|(_, parent_output)| {
|
||||||
output.is_none() || output.as_ref() == Some(*parent_output)
|
parent_output.is_none()
|
||||||
|
|| output.is_none()
|
||||||
|
|| output.as_ref() == *parent_output
|
||||||
})
|
})
|
||||||
.map(|(mapped, _)| mapped.window.clone());
|
.map(|(mapped, _)| mapped.window.clone());
|
||||||
|
|
||||||
@@ -133,34 +171,34 @@ impl CompositorHandler for State {
|
|||||||
let mapped = Mapped::new(window, rules, hook);
|
let mapped = Mapped::new(window, rules, hook);
|
||||||
let window = mapped.window.clone();
|
let window = mapped.window.clone();
|
||||||
|
|
||||||
let output = if let Some(p) = parent {
|
let target = if let Some(p) = &parent {
|
||||||
// Open dialogs immediately to the right of their parent window.
|
// Open dialogs next to their parent window.
|
||||||
self.niri
|
AddWindowTarget::NextTo(p)
|
||||||
.layout
|
} else if let Some(id) = workspace_id {
|
||||||
.add_window_right_of(&p, mapped, width, is_full_width)
|
AddWindowTarget::Workspace(id)
|
||||||
} else if let Some(workspace_name) = &workspace_name {
|
|
||||||
self.niri.layout.add_window_to_named_workspace(
|
|
||||||
workspace_name,
|
|
||||||
mapped,
|
|
||||||
width,
|
|
||||||
is_full_width,
|
|
||||||
)
|
|
||||||
} else if let Some(output) = &output {
|
} else if let Some(output) = &output {
|
||||||
self.niri
|
AddWindowTarget::Output(output)
|
||||||
.layout
|
|
||||||
.add_window_on_output(output, mapped, width, is_full_width);
|
|
||||||
Some(output)
|
|
||||||
} else {
|
} 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() {
|
if let Some(output) = output.cloned() {
|
||||||
self.niri.layout.start_open_animation_for_window(&window);
|
self.niri.layout.start_open_animation_for_window(&window);
|
||||||
|
|
||||||
let new_active_window =
|
let new_focus = self.niri.layout.focus().map(|m| &m.window);
|
||||||
self.niri.layout.active_window().map(|(m, _)| &m.window);
|
if new_focus == Some(&window) {
|
||||||
if new_active_window == Some(&window) {
|
// We activated the newly opened window.
|
||||||
self.maybe_warp_cursor_to_focus();
|
self.maybe_warp_cursor_to_focus();
|
||||||
|
self.niri.layer_shell_on_demand_focus = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.niri.queue_redraw(&output);
|
self.niri.queue_redraw(&output);
|
||||||
@@ -180,18 +218,12 @@ impl CompositorHandler for State {
|
|||||||
// This is a commit of a previously-mapped root or a non-toplevel root.
|
// This is a commit of a previously-mapped root or a non-toplevel root.
|
||||||
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(surface) {
|
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(surface) {
|
||||||
let window = mapped.window.clone();
|
let window = mapped.window.clone();
|
||||||
let output = output.clone();
|
let output = output.cloned();
|
||||||
|
|
||||||
#[cfg(feature = "xdp-gnome-screencast")]
|
|
||||||
let id = mapped.id();
|
let id = mapped.id();
|
||||||
|
|
||||||
// This is a commit of a previously-mapped toplevel.
|
// This is a commit of a previously-mapped toplevel.
|
||||||
let is_mapped =
|
let is_mapped = is_mapped(surface);
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
// Must start the close animation before window.on_commit().
|
// Must start the close animation before window.on_commit().
|
||||||
let transaction = Transaction::new();
|
let transaction = Transaction::new();
|
||||||
@@ -210,14 +242,11 @@ impl CompositorHandler for State {
|
|||||||
// The toplevel got unmapped.
|
// The toplevel got unmapped.
|
||||||
//
|
//
|
||||||
// Test client: wleird-unmap.
|
// Test client: wleird-unmap.
|
||||||
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
|
let active_window = self.niri.layout.focus().map(|m| &m.window);
|
||||||
let was_active = active_window == Some(&window);
|
let was_active = active_window == Some(&window);
|
||||||
|
|
||||||
#[cfg(feature = "xdp-gnome-screencast")]
|
|
||||||
self.niri
|
self.niri
|
||||||
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
|
.stop_casts_for_target(CastTarget::Window { id: id.get() });
|
||||||
id: id.get(),
|
|
||||||
});
|
|
||||||
|
|
||||||
self.niri.layout.remove_window(&window, transaction.clone());
|
self.niri.layout.remove_window(&window, transaction.clone());
|
||||||
self.add_default_dmabuf_pre_commit_hook(surface);
|
self.add_default_dmabuf_pre_commit_hook(surface);
|
||||||
@@ -237,18 +266,27 @@ impl CompositorHandler for State {
|
|||||||
let unmapped = Unmapped::new(window);
|
let unmapped = Unmapped::new(window);
|
||||||
self.niri.unmapped_windows.insert(surface.clone(), unmapped);
|
self.niri.unmapped_windows.insert(surface.clone(), unmapped);
|
||||||
|
|
||||||
self.niri.queue_redraw(&output);
|
if let Some(output) = output {
|
||||||
|
self.niri.queue_redraw(&output);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let serial = with_states(surface, |states| {
|
let (serial, buffer_delta) = with_states(surface, |states| {
|
||||||
|
let buffer_delta = states
|
||||||
|
.cached_state
|
||||||
|
.get::<SurfaceAttributes>()
|
||||||
|
.current()
|
||||||
|
.buffer_delta
|
||||||
|
.take();
|
||||||
|
|
||||||
let role = states
|
let role = states
|
||||||
.data_map
|
.data_map
|
||||||
.get::<XdgToplevelSurfaceData>()
|
.get::<XdgToplevelSurfaceData>()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
role.configure_serial
|
(role.configure_serial, buffer_delta)
|
||||||
});
|
});
|
||||||
if serial.is_none() {
|
if serial.is_none() {
|
||||||
error!("commit on a mapped surface without a configured serial");
|
error!("commit on a mapped surface without a configured serial");
|
||||||
@@ -257,10 +295,25 @@ impl CompositorHandler for State {
|
|||||||
// The toplevel remains mapped.
|
// The toplevel remains mapped.
|
||||||
self.niri.layout.update_window(&window, serial);
|
self.niri.layout.update_window(&window, serial);
|
||||||
|
|
||||||
// Popup placement depends on window size which might have changed.
|
// Move the toplevel according to the attach offset.
|
||||||
self.update_reactive_popups(&window, &output);
|
if let Some(delta) = buffer_delta {
|
||||||
|
if delta.x != 0 || delta.y != 0 {
|
||||||
|
let (x, y) = delta.to_f64().into();
|
||||||
|
self.niri.layout.move_floating_window(
|
||||||
|
Some(&window),
|
||||||
|
PositionChange::AdjustFixed(x),
|
||||||
|
PositionChange::AdjustFixed(y),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.niri.queue_redraw(&output);
|
// Popup placement depends on window size which might have changed.
|
||||||
|
self.update_reactive_popups(&window);
|
||||||
|
|
||||||
|
if let Some(output) = output {
|
||||||
|
self.niri.queue_redraw(&output);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,10 +324,12 @@ impl CompositorHandler for State {
|
|||||||
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
|
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
|
||||||
if let Some((mapped, output)) = root_window_output {
|
if let Some((mapped, output)) = root_window_output {
|
||||||
let window = mapped.window.clone();
|
let window = mapped.window.clone();
|
||||||
let output = output.clone();
|
let output = output.cloned();
|
||||||
window.on_commit();
|
window.on_commit();
|
||||||
self.niri.layout.update_window(&window, None);
|
self.niri.layout.update_window(&window, None);
|
||||||
self.niri.queue_redraw(&output);
|
if let Some(output) = output {
|
||||||
|
self.niri.queue_redraw(&output);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,16 +404,23 @@ impl CompositorHandler for State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This might be a lock surface.
|
// This might be a lock surface.
|
||||||
if self.niri.is_locked() {
|
for (output, state) in &self.niri.output_state {
|
||||||
for (output, state) in &self.niri.output_state {
|
if let Some(lock_surface) = &state.lock_surface {
|
||||||
if let Some(lock_surface) = &state.lock_surface {
|
if lock_surface.wl_surface() == &root_surface {
|
||||||
if lock_surface.wl_surface() == &root_surface {
|
if matches!(self.niri.lock_state, LockState::WaitingForSurfaces { .. }) {
|
||||||
|
self.niri.maybe_continue_to_locking();
|
||||||
|
} else {
|
||||||
self.niri.queue_redraw(&output.clone());
|
self.niri.queue_redraw(&output.clone());
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This message can trigger for lock surfaces that had a commit right after we unlocked
|
||||||
|
// the session, but that's ok, we don't need to handle them.
|
||||||
|
trace!("commit on an unrecognized surface: {surface:?}, root: {root_surface:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn destroyed(&mut self, surface: &WlSurface) {
|
fn destroyed(&mut self, surface: &WlSurface) {
|
||||||
|
|||||||
+50
-10
@@ -1,4 +1,3 @@
|
|||||||
use smithay::backend::renderer::utils::with_renderer_surface_state;
|
|
||||||
use smithay::delegate_layer_shell;
|
use smithay::delegate_layer_shell;
|
||||||
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
|
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
|
||||||
use smithay::output::Output;
|
use smithay::output::Output;
|
||||||
@@ -11,8 +10,9 @@ use smithay::wayland::shell::wlr_layer::{
|
|||||||
};
|
};
|
||||||
use smithay::wayland::shell::xdg::PopupSurface;
|
use smithay::wayland::shell::xdg::PopupSurface;
|
||||||
|
|
||||||
|
use crate::layer::{MappedLayer, ResolvedLayerRules};
|
||||||
use crate::niri::State;
|
use crate::niri::State;
|
||||||
use crate::utils::send_scale_transform;
|
use crate::utils::{is_mapped, output_size, send_scale_transform};
|
||||||
|
|
||||||
impl WlrLayerShellHandler for State {
|
impl WlrLayerShellHandler for State {
|
||||||
fn shell_state(&mut self) -> &mut WlrLayerShellState {
|
fn shell_state(&mut self) -> &mut WlrLayerShellState {
|
||||||
@@ -60,6 +60,7 @@ impl WlrLayerShellHandler for State {
|
|||||||
layer.map(|layer| (o.clone(), map, layer))
|
layer.map(|layer| (o.clone(), map, layer))
|
||||||
}) {
|
}) {
|
||||||
map.unmap_layer(&layer);
|
map.unmap_layer(&layer);
|
||||||
|
self.niri.mapped_layer_surfaces.remove(&layer);
|
||||||
Some(output)
|
Some(output)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -118,16 +119,38 @@ impl State {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
if initial_configure_sent {
|
if initial_configure_sent {
|
||||||
let is_mapped =
|
if is_mapped(surface) {
|
||||||
with_renderer_surface_state(surface, |state| state.buffer().is_some())
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
error!("no renderer surface state even though we use commit handler");
|
|
||||||
false
|
|
||||||
});
|
|
||||||
|
|
||||||
if is_mapped {
|
|
||||||
let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface);
|
let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface);
|
||||||
|
|
||||||
|
// Resolve rules for newly mapped layer surfaces.
|
||||||
|
if was_unmapped {
|
||||||
|
let config = self.niri.config.borrow();
|
||||||
|
|
||||||
|
let rules = &config.layer_rules;
|
||||||
|
let rules =
|
||||||
|
ResolvedLayerRules::compute(rules, layer, self.niri.is_at_startup);
|
||||||
|
|
||||||
|
let output_size = output_size(&output);
|
||||||
|
let scale = output.current_scale().fractional_scale();
|
||||||
|
|
||||||
|
let mapped = MappedLayer::new(
|
||||||
|
layer.clone(),
|
||||||
|
rules,
|
||||||
|
output_size,
|
||||||
|
scale,
|
||||||
|
self.niri.clock.clone(),
|
||||||
|
&config,
|
||||||
|
);
|
||||||
|
|
||||||
|
let prev = self
|
||||||
|
.niri
|
||||||
|
.mapped_layer_surfaces
|
||||||
|
.insert(layer.clone(), mapped);
|
||||||
|
if prev.is_some() {
|
||||||
|
error!("MappedLayer was present for an unmapped surface");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Give focus to newly mapped on-demand surfaces. Some launchers like
|
// Give focus to newly mapped on-demand surfaces. Some launchers like
|
||||||
// lxqt-runner rely on this behavior. While this behavior doesn't make much
|
// 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
|
// sense for other clients like panels, the consensus seems to be that it's not
|
||||||
@@ -151,7 +174,24 @@ impl State {
|
|||||||
self.niri.layer_shell_on_demand_focus = Some(layer.clone());
|
self.niri.layer_shell_on_demand_focus = Some(layer.clone());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
let was_mapped = self.niri.mapped_layer_surfaces.remove(layer).is_some();
|
||||||
self.niri.unmapped_layer_surfaces.insert(surface.clone());
|
self.niri.unmapped_layer_surfaces.insert(surface.clone());
|
||||||
|
|
||||||
|
// After layer surface unmaps it has to perform the initial commit-configure
|
||||||
|
// sequence again. This is a workaround until Smithay properly resets
|
||||||
|
// initial_configure_sent upon the surface unmapping itself as it does for
|
||||||
|
// toplevels.
|
||||||
|
if was_mapped {
|
||||||
|
with_states(surface, |states| {
|
||||||
|
let mut data = states
|
||||||
|
.data_map
|
||||||
|
.get::<LayerSurfaceData>()
|
||||||
|
.unwrap()
|
||||||
|
.lock()
|
||||||
|
.unwrap();
|
||||||
|
data.initial_configure_sent = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let scale = output.current_scale();
|
let scale = output.current_scale();
|
||||||
|
|||||||
+179
-41
@@ -11,7 +11,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||||
use smithay::backend::drm::DrmNode;
|
use smithay::backend::drm::DrmNode;
|
||||||
use smithay::backend::input::TabletToolDescriptor;
|
use smithay::backend::input::{InputEvent, TabletToolDescriptor};
|
||||||
use smithay::desktop::{PopupKind, PopupManager};
|
use smithay::desktop::{PopupKind, PopupManager};
|
||||||
use smithay::input::pointer::{
|
use smithay::input::pointer::{
|
||||||
CursorIcon, CursorImageStatus, CursorImageSurfaceData, PointerHandle,
|
CursorIcon, CursorImageStatus, CursorImageSurfaceData, PointerHandle,
|
||||||
@@ -35,6 +35,9 @@ use smithay::wayland::fractional_scale::FractionalScaleHandler;
|
|||||||
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
|
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
|
||||||
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
|
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
|
||||||
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
|
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
|
||||||
|
use smithay::wayland::keyboard_shortcuts_inhibit::{
|
||||||
|
KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor,
|
||||||
|
};
|
||||||
use smithay::wayland::output::OutputHandler;
|
use smithay::wayland::output::OutputHandler;
|
||||||
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler};
|
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler};
|
||||||
use smithay::wayland::security_context::{
|
use smithay::wayland::security_context::{
|
||||||
@@ -44,10 +47,15 @@ use smithay::wayland::selection::data_device::{
|
|||||||
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
|
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
|
||||||
ServerDndGrabHandler,
|
ServerDndGrabHandler,
|
||||||
};
|
};
|
||||||
|
use smithay::wayland::selection::ext_data_control::{
|
||||||
|
DataControlHandler as ExtDataControlHandler, DataControlState as ExtDataControlState,
|
||||||
|
};
|
||||||
use smithay::wayland::selection::primary_selection::{
|
use smithay::wayland::selection::primary_selection::{
|
||||||
set_primary_focus, PrimarySelectionHandler, PrimarySelectionState,
|
set_primary_focus, PrimarySelectionHandler, PrimarySelectionState,
|
||||||
};
|
};
|
||||||
use smithay::wayland::selection::wlr_data_control::{DataControlHandler, DataControlState};
|
use smithay::wayland::selection::wlr_data_control::{
|
||||||
|
DataControlHandler as WlrDataControlHandler, DataControlState as WlrDataControlState,
|
||||||
|
};
|
||||||
use smithay::wayland::selection::{SelectionHandler, SelectionTarget};
|
use smithay::wayland::selection::{SelectionHandler, SelectionTarget};
|
||||||
use smithay::wayland::session_lock::{
|
use smithay::wayland::session_lock::{
|
||||||
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
|
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
|
||||||
@@ -58,16 +66,18 @@ use smithay::wayland::xdg_activation::{
|
|||||||
};
|
};
|
||||||
use smithay::{
|
use smithay::{
|
||||||
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
|
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
|
||||||
delegate_drm_lease, delegate_fractional_scale, delegate_idle_inhibit, delegate_idle_notify,
|
delegate_drm_lease, delegate_ext_data_control, delegate_fractional_scale,
|
||||||
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
|
delegate_idle_inhibit, delegate_idle_notify, delegate_input_method_manager,
|
||||||
|
delegate_keyboard_shortcuts_inhibit, delegate_output, delegate_pointer_constraints,
|
||||||
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
|
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
|
||||||
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
|
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
|
||||||
delegate_tablet_manager, delegate_text_input_manager, delegate_viewporter,
|
delegate_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager,
|
||||||
delegate_virtual_keyboard_manager, delegate_xdg_activation,
|
delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
|
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
|
||||||
use crate::niri::{ClientState, DndIcon, State};
|
use crate::layout::ActivateWindow;
|
||||||
|
use crate::niri::{DndIcon, NewClient, State};
|
||||||
use crate::protocols::foreign_toplevel::{
|
use crate::protocols::foreign_toplevel::{
|
||||||
self, ForeignToplevelHandler, ForeignToplevelManagerState,
|
self, ForeignToplevelHandler, ForeignToplevelManagerState,
|
||||||
};
|
};
|
||||||
@@ -75,10 +85,15 @@ use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerSt
|
|||||||
use crate::protocols::mutter_x11_interop::MutterX11InteropHandler;
|
use crate::protocols::mutter_x11_interop::MutterX11InteropHandler;
|
||||||
use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState};
|
use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState};
|
||||||
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState};
|
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState};
|
||||||
|
use crate::protocols::virtual_pointer::{
|
||||||
|
VirtualPointerAxisEvent, VirtualPointerButtonEvent, VirtualPointerHandler,
|
||||||
|
VirtualPointerInputBackend, VirtualPointerManagerState, VirtualPointerMotionAbsoluteEvent,
|
||||||
|
VirtualPointerMotionEvent,
|
||||||
|
};
|
||||||
use crate::utils::{output_size, send_scale_transform, with_toplevel_role};
|
use crate::utils::{output_size, send_scale_transform, with_toplevel_role};
|
||||||
use crate::{
|
use crate::{
|
||||||
delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop,
|
delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop,
|
||||||
delegate_output_management, delegate_screencopy,
|
delegate_output_management, delegate_screencopy, delegate_virtual_pointer,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10);
|
pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
@@ -155,7 +170,7 @@ impl PointerConstraintsHandler for State {
|
|||||||
location: Point<f64, Logical>,
|
location: Point<f64, Logical>,
|
||||||
) {
|
) {
|
||||||
let is_constraint_active = with_pointer_constraint(surface, pointer, |constraint| {
|
let is_constraint_active = with_pointer_constraint(surface, pointer, |constraint| {
|
||||||
constraint.map_or(false, |c| c.is_active())
|
constraint.is_some_and(|c| c.is_active())
|
||||||
});
|
});
|
||||||
|
|
||||||
if !is_constraint_active {
|
if !is_constraint_active {
|
||||||
@@ -196,7 +211,7 @@ impl PointerConstraintsHandler for State {
|
|||||||
pointer.set_location(target);
|
pointer.set_location(target);
|
||||||
|
|
||||||
// Redraw to update the cursor position if it's visible.
|
// Redraw to update the cursor position if it's visible.
|
||||||
if !self.niri.pointer_hidden {
|
if self.niri.pointer_visibility.is_visible() {
|
||||||
// FIXME: redraw only outputs overlapping the cursor.
|
// FIXME: redraw only outputs overlapping the cursor.
|
||||||
self.niri.queue_redraw_all();
|
self.niri.queue_redraw_all();
|
||||||
}
|
}
|
||||||
@@ -243,7 +258,28 @@ impl InputMethodHandler for State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl KeyboardShortcutsInhibitHandler for State {
|
||||||
|
fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState {
|
||||||
|
&mut self.niri.keyboard_shortcuts_inhibit_state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
|
||||||
|
// FIXME: show a confirmation dialog with a "remember for this application" kind of toggle.
|
||||||
|
inhibitor.activate();
|
||||||
|
self.niri
|
||||||
|
.keyboard_shortcuts_inhibiting_surfaces
|
||||||
|
.insert(inhibitor.wl_surface().clone(), inhibitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inhibitor_destroyed(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
|
||||||
|
self.niri
|
||||||
|
.keyboard_shortcuts_inhibiting_surfaces
|
||||||
|
.remove(&inhibitor.wl_surface().clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate_input_method_manager!(State);
|
delegate_input_method_manager!(State);
|
||||||
|
delegate_keyboard_shortcuts_inhibit!(State);
|
||||||
delegate_virtual_keyboard_manager!(State);
|
delegate_virtual_keyboard_manager!(State);
|
||||||
|
|
||||||
impl SelectionHandler for State {
|
impl SelectionHandler for State {
|
||||||
@@ -306,7 +342,42 @@ impl ClientDndGrabHandler for State {
|
|||||||
self.niri.queue_redraw_all();
|
self.niri.queue_redraw_all();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dropped(&mut self, _seat: Seat<Self>) {
|
fn dropped(&mut self, target: Option<WlSurface>, validated: bool, _seat: Seat<Self>) {
|
||||||
|
trace!("client dropped, target: {target:?}, validated: {validated}");
|
||||||
|
|
||||||
|
// End DnD before activating a specific window below so that it takes precedence.
|
||||||
|
self.niri.layout.dnd_end();
|
||||||
|
|
||||||
|
// Activate the target output, since that's how Firefox drag-tab-into-new-window works for
|
||||||
|
// example. On successful drop, additionally activate the target window.
|
||||||
|
let mut activate_output = true;
|
||||||
|
if let Some(target) = validated.then_some(target).flatten() {
|
||||||
|
let root = self.niri.find_root_shell_surface(&target);
|
||||||
|
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&root) {
|
||||||
|
let window = mapped.window.clone();
|
||||||
|
self.niri.layout.activate_window(&window);
|
||||||
|
self.niri.layer_shell_on_demand_focus = None;
|
||||||
|
activate_output = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if activate_output {
|
||||||
|
// Find the output from cursor coordinates.
|
||||||
|
//
|
||||||
|
// FIXME: uhhh, we can't actually properly tell if the DnD comes from pointer or touch,
|
||||||
|
// and if it comes from touch, then what the coordinates are. Need to pass more
|
||||||
|
// parameters from Smithay I guess.
|
||||||
|
//
|
||||||
|
// Assume that hidden pointer means touch DnD.
|
||||||
|
if self.niri.pointer_visibility.is_visible() {
|
||||||
|
// We can't even get the current pointer location because it's locked (we're deep
|
||||||
|
// in the grab call stack here). So use the last known one.
|
||||||
|
if let Some(output) = &self.niri.pointer_contents.output {
|
||||||
|
self.niri.layout.focus_output(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.niri.dnd_icon = None;
|
self.niri.dnd_icon = None;
|
||||||
// FIXME: more granular
|
// FIXME: more granular
|
||||||
self.niri.queue_redraw_all();
|
self.niri.queue_redraw_all();
|
||||||
@@ -324,14 +395,22 @@ impl PrimarySelectionHandler for State {
|
|||||||
}
|
}
|
||||||
delegate_primary_selection!(State);
|
delegate_primary_selection!(State);
|
||||||
|
|
||||||
impl DataControlHandler for State {
|
impl WlrDataControlHandler for State {
|
||||||
fn data_control_state(&self) -> &DataControlState {
|
fn data_control_state(&self) -> &WlrDataControlState {
|
||||||
&self.niri.data_control_state
|
&self.niri.wlr_data_control_state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate_data_control!(State);
|
delegate_data_control!(State);
|
||||||
|
|
||||||
|
impl ExtDataControlHandler for State {
|
||||||
|
fn data_control_state(&self) -> &ExtDataControlState {
|
||||||
|
&self.niri.ext_data_control_state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate_ext_data_control!(State);
|
||||||
|
|
||||||
impl OutputHandler for State {
|
impl OutputHandler for State {
|
||||||
fn output_bound(&mut self, output: Output, wl_output: WlOutput) {
|
fn output_bound(&mut self, output: Output, wl_output: WlOutput) {
|
||||||
foreign_toplevel::on_output_bound(self, &output, &wl_output);
|
foreign_toplevel::on_output_bound(self, &output, &wl_output);
|
||||||
@@ -372,6 +451,8 @@ impl SessionLockHandler for State {
|
|||||||
|
|
||||||
fn unlock(&mut self) {
|
fn unlock(&mut self) {
|
||||||
self.niri.unlock();
|
self.niri.unlock();
|
||||||
|
self.niri.activate_monitors(&mut self.backend);
|
||||||
|
self.niri.notify_activity();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
|
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
|
||||||
@@ -405,18 +486,12 @@ impl SecurityContextHandler for State {
|
|||||||
self.niri
|
self.niri
|
||||||
.event_loop
|
.event_loop
|
||||||
.insert_source(source, move |client, _, state| {
|
.insert_source(source, move |client, _, state| {
|
||||||
let config = state.niri.config.borrow();
|
trace!("inserting a new restricted client, context={context:?}");
|
||||||
let data = Arc::new(ClientState {
|
state.niri.insert_client(NewClient {
|
||||||
compositor_state: Default::default(),
|
client,
|
||||||
can_view_decoration_globals: config.prefer_no_csd,
|
|
||||||
restricted: true,
|
restricted: true,
|
||||||
|
credentials_unknown: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
|
|
||||||
warn!("error inserting client: {err}");
|
|
||||||
} else {
|
|
||||||
trace!("inserted a new restricted client, context={context:?}");
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
@@ -476,10 +551,13 @@ impl ForeignToplevelHandler for State {
|
|||||||
let window = mapped.window.clone();
|
let window = mapped.window.clone();
|
||||||
|
|
||||||
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
|
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
|
||||||
if &requested_output != current_output {
|
if Some(&requested_output) != current_output {
|
||||||
self.niri
|
self.niri.layout.move_to_output(
|
||||||
.layout
|
Some(&window),
|
||||||
.move_to_output(Some(&window), &requested_output, None);
|
&requested_output,
|
||||||
|
None,
|
||||||
|
ActivateWindow::Smart,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,6 +602,31 @@ impl ScreencopyHandler for State {
|
|||||||
}
|
}
|
||||||
delegate_screencopy!(State);
|
delegate_screencopy!(State);
|
||||||
|
|
||||||
|
impl VirtualPointerHandler for State {
|
||||||
|
fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState {
|
||||||
|
&mut self.niri.virtual_pointer_state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_virtual_pointer_motion(&mut self, event: VirtualPointerMotionEvent) {
|
||||||
|
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerMotion { event });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent) {
|
||||||
|
self.process_input_event(
|
||||||
|
InputEvent::<VirtualPointerInputBackend>::PointerMotionAbsolute { event },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent) {
|
||||||
|
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerButton { event });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent) {
|
||||||
|
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerAxis { event });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_virtual_pointer!(State);
|
||||||
|
|
||||||
impl DrmLeaseHandler for State {
|
impl DrmLeaseHandler for State {
|
||||||
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
|
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
|
||||||
self.backend
|
self.backend
|
||||||
@@ -604,25 +707,51 @@ impl GammaControlHandler for State {
|
|||||||
}
|
}
|
||||||
delegate_gamma_control!(State);
|
delegate_gamma_control!(State);
|
||||||
|
|
||||||
|
struct UrgentOnlyMarker;
|
||||||
|
|
||||||
impl XdgActivationHandler for State {
|
impl XdgActivationHandler for State {
|
||||||
fn activation_state(&mut self) -> &mut XdgActivationState {
|
fn activation_state(&mut self) -> &mut XdgActivationState {
|
||||||
&mut self.niri.activation_state
|
&mut self.niri.activation_state
|
||||||
}
|
}
|
||||||
|
|
||||||
fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool {
|
fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool {
|
||||||
// Only tokens that were created while the application has keyboard focus are valid.
|
// Tokens without a serial are urgency-only. This is not specified, but it seems to be the
|
||||||
|
// common client behavior.
|
||||||
|
//
|
||||||
|
// See also: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/150
|
||||||
let Some((serial, seat)) = data.serial else {
|
let Some((serial, seat)) = data.serial else {
|
||||||
return false;
|
data.user_data.insert_if_missing(|| UrgentOnlyMarker);
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
let Some(seat) = Seat::<State>::from_resource(&seat) else {
|
let Some(seat) = Seat::<State>::from_resource(&seat) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let keyboard = seat.get_keyboard().unwrap();
|
// Widely-used clients such as Discord and Telegram make new tokens (with invalid serials)
|
||||||
keyboard
|
// upon clicking on their tray icon or on their notification. This debug flag makes that
|
||||||
.last_enter()
|
// work.
|
||||||
.map(|last_enter| serial.is_no_older_than(&last_enter))
|
//
|
||||||
.unwrap_or(false)
|
// Clicking on a notification sends clients a perfectly valid activation token from the
|
||||||
|
// notification daemon, but alas they ignore it. Maybe in the future the clients are fixed,
|
||||||
|
// and we can remove this debug flag.
|
||||||
|
let config = self.niri.config.borrow();
|
||||||
|
if config.debug.honor_xdg_activation_with_invalid_serial {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the serial against both a keyboard and a pointer, since layer-shell surfaces
|
||||||
|
// with no keyboard interactivity won't have any keyboard focus.
|
||||||
|
let kb_last_enter = seat.get_keyboard().unwrap().last_enter();
|
||||||
|
if kb_last_enter.is_some_and(|last_enter| serial.is_no_older_than(&last_enter)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pointer_last_enter = seat.get_pointer().unwrap().last_enter();
|
||||||
|
if pointer_last_enter.is_some_and(|last_enter| serial.is_no_older_than(&last_enter)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_activation(
|
fn request_activation(
|
||||||
@@ -632,15 +761,22 @@ impl XdgActivationHandler for State {
|
|||||||
surface: WlSurface,
|
surface: WlSurface,
|
||||||
) {
|
) {
|
||||||
if token_data.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT {
|
if token_data.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT {
|
||||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&surface) {
|
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(&surface) {
|
||||||
let window = mapped.window.clone();
|
let window = mapped.window.clone();
|
||||||
self.niri.layout.activate_window(&window);
|
if token_data.user_data.get::<UrgentOnlyMarker>().is_some() {
|
||||||
self.niri.layer_shell_on_demand_focus = None;
|
mapped.set_urgent(true);
|
||||||
self.niri.queue_redraw_all();
|
self.niri.queue_redraw_all();
|
||||||
|
} else {
|
||||||
self.niri.activation_state.remove_token(&token);
|
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);
|
delegate_xdg_activation!(State);
|
||||||
@@ -662,3 +798,5 @@ delegate_output_management!(State);
|
|||||||
|
|
||||||
impl MutterX11InteropHandler for State {}
|
impl MutterX11InteropHandler for State {}
|
||||||
delegate_mutter_x11_interop!(State);
|
delegate_mutter_x11_interop!(State);
|
||||||
|
|
||||||
|
delegate_single_pixel_buffer!(State);
|
||||||
|
|||||||
+202
-87
@@ -1,6 +1,7 @@
|
|||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
|
|
||||||
use calloop::Interest;
|
use calloop::Interest;
|
||||||
|
use niri_config::PresetSize;
|
||||||
use smithay::desktop::{
|
use smithay::desktop::{
|
||||||
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, utils, LayerSurface,
|
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, utils, LayerSurface,
|
||||||
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
|
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
|
||||||
@@ -42,10 +43,12 @@ use crate::input::resize_grab::ResizeGrab;
|
|||||||
use crate::input::touch_move_grab::TouchMoveGrab;
|
use crate::input::touch_move_grab::TouchMoveGrab;
|
||||||
use crate::input::touch_resize_grab::TouchResizeGrab;
|
use crate::input::touch_resize_grab::TouchResizeGrab;
|
||||||
use crate::input::{PointerOrTouchStartData, DOUBLE_CLICK_TIME};
|
use crate::input::{PointerOrTouchStartData, DOUBLE_CLICK_TIME};
|
||||||
use crate::layout::workspace::ColumnWidth;
|
use crate::layout::ActivateWindow;
|
||||||
use crate::niri::{PopupGrabState, State};
|
use crate::niri::{CastTarget, PopupGrabState, State};
|
||||||
use crate::utils::transaction::Transaction;
|
use crate::utils::transaction::Transaction;
|
||||||
use crate::utils::{get_monotonic_time, output_matches_name, send_scale_transform, ResizeEdge};
|
use crate::utils::{
|
||||||
|
get_monotonic_time, output_matches_name, send_scale_transform, update_tiled_state, ResizeEdge,
|
||||||
|
};
|
||||||
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
|
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
|
||||||
|
|
||||||
impl XdgShellHandler for State {
|
impl XdgShellHandler for State {
|
||||||
@@ -123,6 +126,10 @@ impl XdgShellHandler for State {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let Some(output) = output else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let window = mapped.window.clone();
|
let window = mapped.window.clone();
|
||||||
let output = output.clone();
|
let output = output.clone();
|
||||||
|
|
||||||
@@ -146,7 +153,7 @@ impl XdgShellHandler for State {
|
|||||||
|
|
||||||
match start_data {
|
match start_data {
|
||||||
PointerOrTouchStartData::Pointer(start_data) => {
|
PointerOrTouchStartData::Pointer(start_data) => {
|
||||||
let grab = MoveGrab::new(start_data, window);
|
let grab = MoveGrab::new(start_data, window, false);
|
||||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||||
}
|
}
|
||||||
PointerOrTouchStartData::Touch(start_data) => {
|
PointerOrTouchStartData::Touch(start_data) => {
|
||||||
@@ -209,8 +216,16 @@ impl XdgShellHandler for State {
|
|||||||
// See if we got a double resize-click gesture.
|
// See if we got a double resize-click gesture.
|
||||||
let time = get_monotonic_time();
|
let time = get_monotonic_time();
|
||||||
let last_cell = mapped.last_interactive_resize_start();
|
let last_cell = mapped.last_interactive_resize_start();
|
||||||
let last = last_cell.get();
|
let mut last = last_cell.get();
|
||||||
last_cell.set(Some((time, edges)));
|
last_cell.set(Some((time, edges)));
|
||||||
|
|
||||||
|
// Floating windows don't have either of the double-resize-click gestures, so just allow it
|
||||||
|
// to resize.
|
||||||
|
if mapped.is_floating() {
|
||||||
|
last = None;
|
||||||
|
last_cell.set(None);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((last_time, last_edges)) = last {
|
if let Some((last_time, last_edges)) = last {
|
||||||
if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME {
|
if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME {
|
||||||
// Allow quick resize after a triple click.
|
// Allow quick resize after a triple click.
|
||||||
@@ -281,6 +296,7 @@ impl XdgShellHandler for State {
|
|||||||
|
|
||||||
let popup = PopupKind::Xdg(surface);
|
let popup = PopupKind::Xdg(surface);
|
||||||
let Ok(root) = find_popup_root_surface(&popup) else {
|
let Ok(root) = find_popup_root_surface(&popup) else {
|
||||||
|
trace!("ignoring popup grab because no root surface");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -289,30 +305,41 @@ impl XdgShellHandler for State {
|
|||||||
// keyboard focus being at the wrong place.
|
// keyboard focus being at the wrong place.
|
||||||
if self.niri.is_locked() {
|
if self.niri.is_locked() {
|
||||||
if Some(&root) != self.niri.lock_surface_focus().as_ref() {
|
if Some(&root) != self.niri.lock_surface_focus().as_ref() {
|
||||||
|
trace!("ignoring popup grab because the session is locked");
|
||||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if self.niri.screenshot_ui.is_open() {
|
} else if self.niri.screenshot_ui.is_open() {
|
||||||
|
trace!("ignoring popup grab because the screenshot UI is open");
|
||||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||||
return;
|
return;
|
||||||
} else if let Some(output) = self.niri.layout.active_output() {
|
} else if let Some(output) = self.niri.layout.active_output() {
|
||||||
let layers = layer_map_for_output(output);
|
let layers = layer_map_for_output(output);
|
||||||
|
|
||||||
if let Some(layer_surface) =
|
// FIXME: somewhere here we probably need to check is_overview_open to match the logic
|
||||||
layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
|
// in update_keyboard_focus().
|
||||||
{
|
|
||||||
if !matches!(layer_surface.layer(), Layer::Overlay | Layer::Top) {
|
|
||||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: popup grabs for on-demand bottom and background layers.
|
if let Some(layer) = layers.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL) {
|
||||||
|
// This is a grab for a layer surface.
|
||||||
|
|
||||||
|
if let Some(mapped) = self.niri.mapped_layer_surfaces.get(layer) {
|
||||||
|
if mapped.place_within_backdrop() {
|
||||||
|
trace!("ignoring popup grab for a layer surface within overview backdrop");
|
||||||
|
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// This is a grab for a regular window; check that there's no layer surface with a
|
||||||
|
// higher input priority.
|
||||||
|
|
||||||
if layers.layers_on(Layer::Overlay).any(|l| {
|
if layers.layers_on(Layer::Overlay).any(|l| {
|
||||||
l.cached_state().keyboard_interactivity
|
(l.cached_state().keyboard_interactivity
|
||||||
== wlr_layer::KeyboardInteractivity::Exclusive
|
== wlr_layer::KeyboardInteractivity::Exclusive
|
||||||
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
|
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref())
|
||||||
|
&& self.niri.mapped_layer_surfaces.contains_key(l)
|
||||||
}) {
|
}) {
|
||||||
|
trace!("ignoring toplevel popup grab because the overlay layer has focus");
|
||||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -320,38 +347,57 @@ impl XdgShellHandler for State {
|
|||||||
let mon = self.niri.layout.monitor_for_output(output).unwrap();
|
let mon = self.niri.layout.monitor_for_output(output).unwrap();
|
||||||
if !mon.render_above_top_layer()
|
if !mon.render_above_top_layer()
|
||||||
&& layers.layers_on(Layer::Top).any(|l| {
|
&& layers.layers_on(Layer::Top).any(|l| {
|
||||||
l.cached_state().keyboard_interactivity
|
(l.cached_state().keyboard_interactivity
|
||||||
== wlr_layer::KeyboardInteractivity::Exclusive
|
== wlr_layer::KeyboardInteractivity::Exclusive
|
||||||
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
|
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref())
|
||||||
|
&& self.niri.mapped_layer_surfaces.contains_key(l)
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
|
trace!("ignoring toplevel popup grab because the top layer has focus");
|
||||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let layout_focus = self.niri.layout.focus();
|
let layout_focus = self.niri.layout.focus();
|
||||||
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
|
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
|
||||||
|
trace!("ignoring toplevel popup grab because another window has focus");
|
||||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
trace!("ignoring popup grab because no output is active");
|
||||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let seat = &self.niri.seat;
|
let seat = &self.niri.seat;
|
||||||
let Ok(mut grab) = self
|
let mut grab = match self
|
||||||
.niri
|
.niri
|
||||||
.popups
|
.popups
|
||||||
.grab_popup(root.clone(), popup, seat, serial)
|
.grab_popup(root.clone(), popup, seat, serial)
|
||||||
else {
|
{
|
||||||
return;
|
Ok(grab) => grab,
|
||||||
|
Err(err) => {
|
||||||
|
trace!("ignoring popup grab: {err:?}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let keyboard = seat.get_keyboard().unwrap();
|
let keyboard = seat.get_keyboard().unwrap();
|
||||||
let pointer = seat.get_pointer().unwrap();
|
let pointer = seat.get_pointer().unwrap();
|
||||||
|
|
||||||
|
let can_receive_keyboard_focus = self
|
||||||
|
.niri
|
||||||
|
.layout
|
||||||
|
.active_output()
|
||||||
|
.and_then(|output| {
|
||||||
|
layer_map_for_output(output)
|
||||||
|
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
|
||||||
|
.map(|layer_surface| layer_surface.can_receive_keyboard_focus())
|
||||||
|
})
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
let keyboard_grab_mismatches = keyboard.is_grabbed()
|
let keyboard_grab_mismatches = keyboard.is_grabbed()
|
||||||
&& !(keyboard.has_grab(serial)
|
&& !(keyboard.has_grab(serial)
|
||||||
|| grab
|
|| grab
|
||||||
@@ -360,16 +406,22 @@ impl XdgShellHandler for State {
|
|||||||
let pointer_grab_mismatches = pointer.is_grabbed()
|
let pointer_grab_mismatches = pointer.is_grabbed()
|
||||||
&& !(pointer.has_grab(serial)
|
&& !(pointer.has_grab(serial)
|
||||||
|| grab.previous_serial().map_or(true, |s| pointer.has_grab(s)));
|
|| grab.previous_serial().map_or(true, |s| pointer.has_grab(s)));
|
||||||
if keyboard_grab_mismatches || pointer_grab_mismatches {
|
if (can_receive_keyboard_focus && keyboard_grab_mismatches) || pointer_grab_mismatches {
|
||||||
|
trace!("ignoring popup grab because of current grab mismatch");
|
||||||
grab.ungrab(PopupUngrabStrategy::All);
|
grab.ungrab(PopupUngrabStrategy::All);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!("new grab for root {:?}", root);
|
trace!("new grab for root {:?}", root);
|
||||||
keyboard.set_focus(self, grab.current_grab(), serial);
|
if can_receive_keyboard_focus {
|
||||||
keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
|
keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
|
||||||
|
}
|
||||||
pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
|
pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
|
||||||
self.niri.popup_grab = Some(PopupGrabState { root, grab });
|
self.niri.popup_grab = Some(PopupGrabState {
|
||||||
|
root,
|
||||||
|
grab,
|
||||||
|
has_keyboard_grab: can_receive_keyboard_focus,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maximize_request(&mut self, surface: ToplevelSurface) {
|
fn maximize_request(&mut self, surface: ToplevelSurface) {
|
||||||
@@ -396,23 +448,26 @@ impl XdgShellHandler for State {
|
|||||||
if let Some((mapped, current_output)) = self
|
if let Some((mapped, current_output)) = self
|
||||||
.niri
|
.niri
|
||||||
.layout
|
.layout
|
||||||
.find_window_and_output(toplevel.wl_surface())
|
.find_window_and_output_mut(toplevel.wl_surface())
|
||||||
{
|
{
|
||||||
|
// A configure is required in response to this event regardless if there are pending
|
||||||
|
// changes.
|
||||||
|
mapped.set_needs_configure();
|
||||||
|
|
||||||
let window = mapped.window.clone();
|
let window = mapped.window.clone();
|
||||||
|
|
||||||
if let Some(requested_output) = requested_output {
|
if let Some(requested_output) = requested_output {
|
||||||
if &requested_output != current_output {
|
if Some(&requested_output) != current_output {
|
||||||
self.niri
|
self.niri.layout.move_to_output(
|
||||||
.layout
|
Some(&window),
|
||||||
.move_to_output(Some(&window), &requested_output, None);
|
&requested_output,
|
||||||
|
None,
|
||||||
|
ActivateWindow::Smart,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.niri.layout.set_fullscreen(&window, true);
|
self.niri.layout.set_fullscreen(&window, true);
|
||||||
|
|
||||||
// A configure is required in response to this event regardless if there are pending
|
|
||||||
// changes.
|
|
||||||
toplevel.send_configure();
|
|
||||||
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
|
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
|
||||||
match &mut unmapped.state {
|
match &mut unmapped.state {
|
||||||
InitialConfigureState::NotConfigured { wants_fullscreen } => {
|
InitialConfigureState::NotConfigured { wants_fullscreen } => {
|
||||||
@@ -434,7 +489,7 @@ impl XdgShellHandler for State {
|
|||||||
toplevel
|
toplevel
|
||||||
.parent()
|
.parent()
|
||||||
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||||
.map(|(_win, output)| output)
|
.and_then(|(_win, output)| output)
|
||||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||||
.map(|mon| (mon, true))
|
.map(|mon| (mon, true))
|
||||||
})
|
})
|
||||||
@@ -459,7 +514,7 @@ impl XdgShellHandler for State {
|
|||||||
toplevel.with_pending_state(|state| {
|
toplevel.with_pending_state(|state| {
|
||||||
state.states.set(xdg_toplevel::State::Fullscreen);
|
state.states.set(xdg_toplevel::State::Fullscreen);
|
||||||
});
|
});
|
||||||
ws.configure_new_window(&unmapped.window, None, rules);
|
ws.configure_new_window(&unmapped.window, None, None, false, rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We already sent the initial configure, so we need to reconfigure.
|
// We already sent the initial configure, so we need to reconfigure.
|
||||||
@@ -476,14 +531,14 @@ impl XdgShellHandler for State {
|
|||||||
if let Some((mapped, _)) = self
|
if let Some((mapped, _)) = self
|
||||||
.niri
|
.niri
|
||||||
.layout
|
.layout
|
||||||
.find_window_and_output(toplevel.wl_surface())
|
.find_window_and_output_mut(toplevel.wl_surface())
|
||||||
{
|
{
|
||||||
let window = mapped.window.clone();
|
|
||||||
self.niri.layout.set_fullscreen(&window, false);
|
|
||||||
|
|
||||||
// A configure is required in response to this event regardless if there are pending
|
// A configure is required in response to this event regardless if there are pending
|
||||||
// changes.
|
// changes.
|
||||||
toplevel.send_configure();
|
mapped.set_needs_configure();
|
||||||
|
|
||||||
|
let window = mapped.window.clone();
|
||||||
|
self.niri.layout.set_fullscreen(&window, false);
|
||||||
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
|
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
|
||||||
match &mut unmapped.state {
|
match &mut unmapped.state {
|
||||||
InitialConfigureState::NotConfigured { wants_fullscreen } => {
|
InitialConfigureState::NotConfigured { wants_fullscreen } => {
|
||||||
@@ -494,6 +549,9 @@ impl XdgShellHandler for State {
|
|||||||
InitialConfigureState::Configured {
|
InitialConfigureState::Configured {
|
||||||
rules,
|
rules,
|
||||||
width,
|
width,
|
||||||
|
height,
|
||||||
|
floating_width,
|
||||||
|
floating_height,
|
||||||
is_full_width,
|
is_full_width,
|
||||||
output,
|
output,
|
||||||
workspace_name,
|
workspace_name,
|
||||||
@@ -517,7 +575,7 @@ impl XdgShellHandler for State {
|
|||||||
.and_then(|parent| {
|
.and_then(|parent| {
|
||||||
self.niri.layout.find_window_and_output(&parent)
|
self.niri.layout.find_window_and_output(&parent)
|
||||||
})
|
})
|
||||||
.map(|(_win, output)| output)
|
.and_then(|(_win, output)| output)
|
||||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||||
.map(|mon| (mon, true))
|
.map(|mon| (mon, true))
|
||||||
})
|
})
|
||||||
@@ -548,12 +606,26 @@ impl XdgShellHandler for State {
|
|||||||
state.states.unset(xdg_toplevel::State::Fullscreen);
|
state.states.unset(xdg_toplevel::State::Fullscreen);
|
||||||
});
|
});
|
||||||
|
|
||||||
let configure_width = if *is_full_width {
|
let is_floating = rules.compute_open_floating(&toplevel);
|
||||||
Some(ColumnWidth::Proportion(1.))
|
let configure_width = if is_floating {
|
||||||
|
*floating_width
|
||||||
|
} else if *is_full_width {
|
||||||
|
Some(PresetSize::Proportion(1.))
|
||||||
} else {
|
} else {
|
||||||
*width
|
*width
|
||||||
};
|
};
|
||||||
ws.configure_new_window(&unmapped.window, configure_width, rules);
|
let configure_height = if is_floating {
|
||||||
|
*floating_height
|
||||||
|
} else {
|
||||||
|
*height
|
||||||
|
};
|
||||||
|
ws.configure_new_window(
|
||||||
|
&unmapped.window,
|
||||||
|
configure_width,
|
||||||
|
configure_height,
|
||||||
|
is_floating,
|
||||||
|
rules,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We already sent the initial configure, so we need to reconfigure.
|
// We already sent the initial configure, so we need to reconfigure.
|
||||||
@@ -589,13 +661,11 @@ impl XdgShellHandler for State {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let window = mapped.window.clone();
|
let window = mapped.window.clone();
|
||||||
let output = output.clone();
|
let output = output.cloned();
|
||||||
|
|
||||||
#[cfg(feature = "xdp-gnome-screencast")]
|
self.niri.stop_casts_for_target(CastTarget::Window {
|
||||||
self.niri
|
id: mapped.id().get(),
|
||||||
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
|
});
|
||||||
id: mapped.id().get(),
|
|
||||||
});
|
|
||||||
|
|
||||||
self.backend.with_primary_renderer(|renderer| {
|
self.backend.with_primary_renderer(|renderer| {
|
||||||
self.niri.layout.store_unmap_snapshot(renderer, &window);
|
self.niri.layout.store_unmap_snapshot(renderer, &window);
|
||||||
@@ -609,7 +679,7 @@ impl XdgShellHandler for State {
|
|||||||
.start_close_animation_for_window(renderer, &window, blocker);
|
.start_close_animation_for_window(renderer, &window, blocker);
|
||||||
});
|
});
|
||||||
|
|
||||||
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
|
let active_window = self.niri.layout.focus().map(|m| &m.window);
|
||||||
let was_active = active_window == Some(&window);
|
let was_active = active_window == Some(&window);
|
||||||
|
|
||||||
self.niri.layout.remove_window(&window, transaction.clone());
|
self.niri.layout.remove_window(&window, transaction.clone());
|
||||||
@@ -625,7 +695,9 @@ impl XdgShellHandler for State {
|
|||||||
self.maybe_warp_cursor_to_focus();
|
self.maybe_warp_cursor_to_focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.niri.queue_redraw(&output);
|
if let Some(output) = output {
|
||||||
|
self.niri.queue_redraw(&output);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn popup_destroyed(&mut self, surface: PopupSurface) {
|
fn popup_destroyed(&mut self, surface: PopupSurface) {
|
||||||
@@ -641,6 +713,22 @@ impl XdgShellHandler for State {
|
|||||||
fn title_changed(&mut self, toplevel: ToplevelSurface) {
|
fn title_changed(&mut self, toplevel: ToplevelSurface) {
|
||||||
self.update_window_rules(&toplevel);
|
self.update_window_rules(&toplevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parent_changed(&mut self, toplevel: ToplevelSurface) {
|
||||||
|
let Some(parent) = toplevel.parent() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((mapped, output)) = self.niri.layout.find_window_and_output_mut(&parent) {
|
||||||
|
let output = output.cloned();
|
||||||
|
let window = mapped.window.clone();
|
||||||
|
if self.niri.layout.descendants_added(&window) {
|
||||||
|
if let Some(output) = output {
|
||||||
|
self.niri.queue_redraw(&output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate_xdg_shell!(State);
|
delegate_xdg_shell!(State);
|
||||||
@@ -669,7 +757,13 @@ impl XdgDecorationHandler for State {
|
|||||||
// A configure is required in response to this event. However, if an initial configure
|
// A configure is required in response to this event. However, if an initial configure
|
||||||
// wasn't sent, then we will send this as part of the initial configure later.
|
// wasn't sent, then we will send this as part of the initial configure later.
|
||||||
if toplevel.is_initial_configure_sent() {
|
if toplevel.is_initial_configure_sent() {
|
||||||
toplevel.send_configure();
|
// If this is a mapped window, flag it as needs configure to avoid duplicate configures.
|
||||||
|
let surface = toplevel.wl_surface();
|
||||||
|
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) {
|
||||||
|
mapped.set_needs_configure();
|
||||||
|
} else {
|
||||||
|
toplevel.send_configure();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,14 +776,20 @@ impl XdgDecorationHandler for State {
|
|||||||
// A configure is required in response to this event. However, if an initial configure
|
// A configure is required in response to this event. However, if an initial configure
|
||||||
// wasn't sent, then we will send this as part of the initial configure later.
|
// wasn't sent, then we will send this as part of the initial configure later.
|
||||||
if toplevel.is_initial_configure_sent() {
|
if toplevel.is_initial_configure_sent() {
|
||||||
toplevel.send_configure();
|
// If this is a mapped window, flag it as needs configure to avoid duplicate configures.
|
||||||
|
let surface = toplevel.wl_surface();
|
||||||
|
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) {
|
||||||
|
mapped.set_needs_configure();
|
||||||
|
} else {
|
||||||
|
toplevel.send_configure();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delegate_xdg_decoration!(State);
|
delegate_xdg_decoration!(State);
|
||||||
|
|
||||||
/// Whether KDE server decorations are in use.
|
/// Whether KDE server decorations are in use.
|
||||||
#[derive(Default)]
|
#[derive(Default, Clone)]
|
||||||
pub struct KdeDecorationsModeState {
|
pub struct KdeDecorationsModeState {
|
||||||
server: Cell<bool>,
|
server: Cell<bool>,
|
||||||
}
|
}
|
||||||
@@ -752,7 +852,7 @@ impl State {
|
|||||||
self.niri.is_at_startup,
|
self.niri.is_at_startup,
|
||||||
);
|
);
|
||||||
|
|
||||||
let Unmapped { window, state } = unmapped;
|
let Unmapped { window, state, .. } = unmapped;
|
||||||
|
|
||||||
let InitialConfigureState::NotConfigured { wants_fullscreen } = state else {
|
let InitialConfigureState::NotConfigured { wants_fullscreen } = state else {
|
||||||
error!("window must not be already configured in send_initial_configure()");
|
error!("window must not be already configured in send_initial_configure()");
|
||||||
@@ -793,7 +893,7 @@ impl State {
|
|||||||
toplevel
|
toplevel
|
||||||
.parent()
|
.parent()
|
||||||
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||||
.map(|(_win, output)| output)
|
.and_then(|(_win, output)| output)
|
||||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||||
.map(|mon| (mon, true))
|
.map(|mon| (mon, true))
|
||||||
});
|
});
|
||||||
@@ -814,7 +914,11 @@ impl State {
|
|||||||
let mon = mon.map(|(mon, _)| mon);
|
let mon = mon.map(|(mon, _)| mon);
|
||||||
|
|
||||||
let mut width = None;
|
let mut width = None;
|
||||||
|
let mut floating_width = None;
|
||||||
|
let mut height = None;
|
||||||
|
let mut floating_height = None;
|
||||||
let is_full_width = rules.open_maximized.unwrap_or(false);
|
let is_full_width = rules.open_maximized.unwrap_or(false);
|
||||||
|
let is_floating = rules.compute_open_floating(toplevel);
|
||||||
|
|
||||||
// Tell the surface the preferred size and bounds for its likely output.
|
// Tell the surface the preferred size and bounds for its likely output.
|
||||||
let ws = rules
|
let ws = rules
|
||||||
@@ -836,31 +940,38 @@ impl State {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
width = ws.resolve_default_width(rules.default_width);
|
width = ws.resolve_default_width(rules.default_width, false);
|
||||||
|
floating_width = ws.resolve_default_width(rules.default_width, true);
|
||||||
|
height = ws.resolve_default_height(rules.default_height, false);
|
||||||
|
floating_height = ws.resolve_default_height(rules.default_height, true);
|
||||||
|
|
||||||
let configure_width = if is_full_width {
|
let configure_width = if is_floating {
|
||||||
Some(ColumnWidth::Proportion(1.))
|
floating_width
|
||||||
|
} else if is_full_width {
|
||||||
|
Some(PresetSize::Proportion(1.))
|
||||||
} else {
|
} else {
|
||||||
width
|
width
|
||||||
};
|
};
|
||||||
ws.configure_new_window(window, configure_width, &rules);
|
let configure_height = if is_floating { floating_height } else { height };
|
||||||
|
ws.configure_new_window(
|
||||||
|
window,
|
||||||
|
configure_width,
|
||||||
|
configure_height,
|
||||||
|
is_floating,
|
||||||
|
&rules,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
|
// Set the tiled state for the initial configure.
|
||||||
// rid of the various client-side rounded corners also by using the tiled state.
|
update_tiled_state(toplevel, config.prefer_no_csd, rules.tiled_state);
|
||||||
if config.prefer_no_csd {
|
|
||||||
toplevel.with_pending_state(|state| {
|
|
||||||
state.states.set(xdg_toplevel::State::TiledLeft);
|
|
||||||
state.states.set(xdg_toplevel::State::TiledRight);
|
|
||||||
state.states.set(xdg_toplevel::State::TiledTop);
|
|
||||||
state.states.set(xdg_toplevel::State::TiledBottom);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the configured settings.
|
// Set the configured settings.
|
||||||
*state = InitialConfigureState::Configured {
|
*state = InitialConfigureState::Configured {
|
||||||
rules,
|
rules,
|
||||||
width,
|
width,
|
||||||
|
height,
|
||||||
|
floating_width,
|
||||||
|
floating_height,
|
||||||
is_full_width,
|
is_full_width,
|
||||||
output,
|
output,
|
||||||
workspace_name: ws.and_then(|w| w.name().cloned()),
|
workspace_name: ws.and_then(|w| w.name().cloned()),
|
||||||
@@ -928,8 +1039,8 @@ impl State {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Figure out if the root is a window or a layer surface.
|
// Figure out if the root is a window or a layer surface.
|
||||||
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(&root) {
|
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&root) {
|
||||||
self.unconstrain_window_popup(popup, &mapped.window, output);
|
self.unconstrain_window_popup(popup, &mapped.window);
|
||||||
} else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| {
|
} else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| {
|
||||||
let map = layer_map_for_output(o);
|
let map = layer_map_for_output(o);
|
||||||
let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
|
let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
|
||||||
@@ -939,19 +1050,10 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unconstrain_window_popup(&self, popup: &PopupKind, window: &Window, output: &Output) {
|
fn unconstrain_window_popup(&self, popup: &PopupKind, window: &Window) {
|
||||||
let window_geo = window.geometry();
|
|
||||||
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
|
|
||||||
|
|
||||||
// The target geometry for the positioner should be relative to its parent's geometry, so
|
// The target geometry for the positioner should be relative to its parent's geometry, so
|
||||||
// we will compute that here.
|
// we will compute that here.
|
||||||
//
|
let mut target = self.niri.layout.popup_target_rect(window);
|
||||||
// We try to keep regular window popups within the window itself horizontally (since the
|
|
||||||
// window can be scrolled to both edges of the screen), but within the whole monitor's
|
|
||||||
// height.
|
|
||||||
let mut target =
|
|
||||||
Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h)).to_f64();
|
|
||||||
target.loc -= self.niri.layout.window_loc(window).unwrap();
|
|
||||||
target.loc -= get_popup_toplevel_coords(popup).to_f64();
|
target.loc -= get_popup_toplevel_coords(popup).to_f64();
|
||||||
|
|
||||||
self.position_popup_within_rect(popup, target);
|
self.position_popup_within_rect(popup, target);
|
||||||
@@ -971,7 +1073,20 @@ impl State {
|
|||||||
|
|
||||||
// The target geometry for the positioner should be relative to its parent's geometry, so
|
// The target geometry for the positioner should be relative to its parent's geometry, so
|
||||||
// we will compute that here.
|
// we will compute that here.
|
||||||
let mut target = Rectangle::from_loc_and_size((0, 0), output_geo.size);
|
let mut target = Rectangle::from_size(output_geo.size);
|
||||||
|
|
||||||
|
// Background and bottom layer popups render below the top and the overlay layer, so let's
|
||||||
|
// put them into the non-exclusive zone.
|
||||||
|
//
|
||||||
|
// FIXME: ideally this should use the "top and overlay layer" non-exclusive zone, but
|
||||||
|
// Smithay only computes the "all layers" non-exclusive zone atm.
|
||||||
|
//
|
||||||
|
// FIXME: related to the above, top layer popups should use the "overlay layer"
|
||||||
|
// non-exclusive zone.
|
||||||
|
if matches!(layer_surface.layer(), Layer::Background | Layer::Bottom) {
|
||||||
|
target = map.non_exclusive_zone();
|
||||||
|
}
|
||||||
|
|
||||||
target.loc -= layer_geo.loc;
|
target.loc -= layer_geo.loc;
|
||||||
target.loc -= get_popup_toplevel_coords(popup);
|
target.loc -= get_popup_toplevel_coords(popup);
|
||||||
|
|
||||||
@@ -1016,7 +1131,7 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_reactive_popups(&self, window: &Window, output: &Output) {
|
pub fn update_reactive_popups(&self, window: &Window) {
|
||||||
let _span = tracy_client::span!("Niri::update_reactive_popups");
|
let _span = tracy_client::span!("Niri::update_reactive_popups");
|
||||||
|
|
||||||
for (popup, _) in PopupManager::popups_for_surface(
|
for (popup, _) in PopupManager::popups_for_surface(
|
||||||
@@ -1025,7 +1140,7 @@ impl State {
|
|||||||
match &popup {
|
match &popup {
|
||||||
xdg_popup @ PopupKind::Xdg(popup) => {
|
xdg_popup @ PopupKind::Xdg(popup) => {
|
||||||
if popup.with_pending_state(|state| state.positioner.reactive) {
|
if popup.with_pending_state(|state| state.positioner.reactive) {
|
||||||
self.unconstrain_window_popup(xdg_popup, window, output);
|
self.unconstrain_window_popup(xdg_popup, window);
|
||||||
if let Err(err) = popup.send_pending_configure() {
|
if let Err(err) = popup.send_pending_configure() {
|
||||||
warn!("error re-configuring reactive popup: {err:?}");
|
warn!("error re-configuring reactive popup: {err:?}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
use ::input as libinput;
|
||||||
|
use smithay::backend::input;
|
||||||
|
use smithay::backend::winit::WinitVirtualDevice;
|
||||||
|
use smithay::output::Output;
|
||||||
|
|
||||||
|
use crate::niri::State;
|
||||||
|
use crate::protocols::virtual_pointer::VirtualPointer;
|
||||||
|
|
||||||
|
pub trait NiriInputBackend: input::InputBackend<Device = Self::NiriDevice> {
|
||||||
|
type NiriDevice: NiriInputDevice;
|
||||||
|
}
|
||||||
|
impl<T: input::InputBackend> NiriInputBackend for T
|
||||||
|
where
|
||||||
|
Self::Device: NiriInputDevice,
|
||||||
|
{
|
||||||
|
type NiriDevice = Self::Device;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait NiriInputDevice: input::Device {
|
||||||
|
// FIXME: this should maybe be per-event, not per-device,
|
||||||
|
// but it's not clear that this matters in practice?
|
||||||
|
// it might be more obvious once we implement it for libinput
|
||||||
|
fn output(&self, state: &State) -> Option<Output>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NiriInputDevice for libinput::Device {
|
||||||
|
fn output(&self, _state: &State) -> Option<Output> {
|
||||||
|
// FIXME: Allow specifying the output per-device?
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NiriInputDevice for WinitVirtualDevice {
|
||||||
|
fn output(&self, _state: &State) -> Option<Output> {
|
||||||
|
// FIXME: we should be returning the single output that the winit backend creates,
|
||||||
|
// but for now, that will cause issues because the output is normally upside down,
|
||||||
|
// so we apply Transform::Flipped180 to it and that would also cause
|
||||||
|
// the cursor position to be flipped, which is not what we want.
|
||||||
|
//
|
||||||
|
// instead, we just return None and rely on the fact that it has only one output.
|
||||||
|
// doing so causes the cursor to be placed in *global* output coordinates,
|
||||||
|
// which are not flipped, and happen to be what we want.
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NiriInputDevice for VirtualPointer {
|
||||||
|
fn output(&self, _: &State) -> Option<Output> {
|
||||||
|
self.output().cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
+1837
-324
File diff suppressed because it is too large
Load Diff
+52
-36
@@ -1,12 +1,11 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use smithay::backend::input::ButtonState;
|
use smithay::backend::input::ButtonState;
|
||||||
use smithay::desktop::Window;
|
use smithay::desktop::Window;
|
||||||
use smithay::input::pointer::{
|
use smithay::input::pointer::{
|
||||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent,
|
||||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent,
|
||||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
|
GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
|
||||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
|
||||||
|
RelativeMotionEvent,
|
||||||
};
|
};
|
||||||
use smithay::input::SeatHandler;
|
use smithay::input::SeatHandler;
|
||||||
use smithay::utils::{IsAlive, Logical, Point};
|
use smithay::utils::{IsAlive, Logical, Point};
|
||||||
@@ -17,16 +16,32 @@ pub struct MoveGrab {
|
|||||||
start_data: PointerGrabStartData<State>,
|
start_data: PointerGrabStartData<State>,
|
||||||
last_location: Point<f64, Logical>,
|
last_location: Point<f64, Logical>,
|
||||||
window: Window,
|
window: Window,
|
||||||
is_moving: bool,
|
gesture: GestureState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum GestureState {
|
||||||
|
Recognizing,
|
||||||
|
Move,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MoveGrab {
|
impl MoveGrab {
|
||||||
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
|
pub fn new(
|
||||||
|
start_data: PointerGrabStartData<State>,
|
||||||
|
window: Window,
|
||||||
|
use_threshold: bool,
|
||||||
|
) -> Self {
|
||||||
|
let gesture = if use_threshold {
|
||||||
|
GestureState::Recognizing
|
||||||
|
} else {
|
||||||
|
GestureState::Move
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
last_location: start_data.location,
|
last_location: start_data.location,
|
||||||
start_data,
|
start_data,
|
||||||
window,
|
window,
|
||||||
is_moving: false,
|
gesture,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +72,24 @@ impl PointerGrab<State> for MoveGrab {
|
|||||||
let output = output.clone();
|
let output = output.clone();
|
||||||
let event_delta = event.location - self.last_location;
|
let event_delta = event.location - self.last_location;
|
||||||
self.last_location = event.location;
|
self.last_location = event.location;
|
||||||
|
|
||||||
|
if self.gesture == GestureState::Recognizing {
|
||||||
|
let c = event.location - self.start_data.location;
|
||||||
|
|
||||||
|
// Check if the gesture moved far enough to decide.
|
||||||
|
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
||||||
|
self.gesture = GestureState::Move;
|
||||||
|
|
||||||
|
data.niri
|
||||||
|
.cursor_manager
|
||||||
|
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.gesture != GestureState::Move {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let ongoing = data.niri.layout.interactive_move_update(
|
let ongoing = data.niri.layout.interactive_move_update(
|
||||||
&self.window,
|
&self.window,
|
||||||
event_delta,
|
event_delta,
|
||||||
@@ -64,14 +97,6 @@ impl PointerGrab<State> for MoveGrab {
|
|||||||
pos_within_output,
|
pos_within_output,
|
||||||
);
|
);
|
||||||
if ongoing {
|
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.
|
// FIXME: only redraw the previous and the new output.
|
||||||
data.niri.queue_redraw_all();
|
data.niri.queue_redraw_all();
|
||||||
return;
|
return;
|
||||||
@@ -104,27 +129,18 @@ impl PointerGrab<State> for MoveGrab {
|
|||||||
) {
|
) {
|
||||||
handle.button(data, event);
|
handle.button(data, event);
|
||||||
|
|
||||||
// MouseButton::Middle
|
// When moving with the left button, right toggles floating, and vice versa.
|
||||||
if event.button == 0x112 {
|
let toggle_floating_button = if self.start_data.button == 0x110 {
|
||||||
if event.state == ButtonState::Pressed {
|
0x111
|
||||||
let output = data
|
} else {
|
||||||
.niri
|
0x110
|
||||||
.output_under(handle.current_location())
|
};
|
||||||
.map(|(output, _)| output)
|
if event.button == toggle_floating_button && event.state == ButtonState::Pressed {
|
||||||
.cloned();
|
data.niri.layout.toggle_window_floating(Some(&self.window));
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if handle.current_pressed().is_empty() {
|
if !handle.current_pressed().contains(&self.start_data.button) {
|
||||||
// No more buttons are pressed, release the grab.
|
// The button that initiated the grab was released.
|
||||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
use niri_ipc::PickedColor;
|
||||||
|
use smithay::backend::allocator::Fourcc;
|
||||||
|
use smithay::backend::input::ButtonState;
|
||||||
|
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||||
|
use smithay::input::pointer::{
|
||||||
|
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||||
|
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||||
|
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
|
||||||
|
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||||
|
};
|
||||||
|
use smithay::input::SeatHandler;
|
||||||
|
use smithay::utils::{Logical, Physical, Point, Scale, Size, Transform};
|
||||||
|
|
||||||
|
use crate::niri::State;
|
||||||
|
use crate::render_helpers::{render_to_vec, RenderTarget};
|
||||||
|
|
||||||
|
pub struct PickColorGrab {
|
||||||
|
start_data: PointerGrabStartData<State>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickColorGrab {
|
||||||
|
pub fn new(start_data: PointerGrabStartData<State>) -> Self {
|
||||||
|
Self { start_data }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_ungrab(&mut self, state: &mut State) {
|
||||||
|
if let Some(tx) = state.niri.pick_color.take() {
|
||||||
|
let _ = tx.send_blocking(None);
|
||||||
|
}
|
||||||
|
state
|
||||||
|
.niri
|
||||||
|
.cursor_manager
|
||||||
|
.set_cursor_image(CursorImageStatus::default_named());
|
||||||
|
state.niri.queue_redraw_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pick_color_at_point(location: Point<f64, Logical>, data: &mut State) -> Option<PickedColor> {
|
||||||
|
let (output, pos_within_output) = data.niri.output_under(location)?;
|
||||||
|
let output = output.clone();
|
||||||
|
|
||||||
|
data.backend
|
||||||
|
.with_primary_renderer(|renderer| {
|
||||||
|
data.niri.update_render_elements(Some(&output));
|
||||||
|
|
||||||
|
let scale = Scale::from(output.current_scale().fractional_scale());
|
||||||
|
// FIXME: perhaps replace floor with round once we figure out the pointer behavior
|
||||||
|
// at the bottom/right edges of the monitors.
|
||||||
|
let pos = pos_within_output.to_physical_precise_floor(scale);
|
||||||
|
let size = Size::<i32, Physical>::from((1, 1));
|
||||||
|
|
||||||
|
let elements = data.niri.render(
|
||||||
|
renderer,
|
||||||
|
&output,
|
||||||
|
false,
|
||||||
|
// This is an interactive operation so we can render without blocking out.
|
||||||
|
RenderTarget::Output,
|
||||||
|
);
|
||||||
|
|
||||||
|
let pixels = match render_to_vec(
|
||||||
|
renderer,
|
||||||
|
size,
|
||||||
|
scale,
|
||||||
|
Transform::Normal,
|
||||||
|
Fourcc::Abgr8888,
|
||||||
|
elements.iter().rev().map(|elem| {
|
||||||
|
let offset = pos.upscale(-1);
|
||||||
|
RelocateRenderElement::from_element(elem, offset, Relocate::Relative)
|
||||||
|
}),
|
||||||
|
) {
|
||||||
|
Ok(pixels) => pixels,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if pixels.len() == 4 {
|
||||||
|
let rgb = [
|
||||||
|
f64::from(pixels[0]) / 255.0,
|
||||||
|
f64::from(pixels[1]) / 255.0,
|
||||||
|
f64::from(pixels[2]) / 255.0,
|
||||||
|
];
|
||||||
|
Some(PickedColor { rgb })
|
||||||
|
} else {
|
||||||
|
error!(
|
||||||
|
"unexpected pixel data length: {} (expected 4)",
|
||||||
|
pixels.len()
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerGrab<State> for PickColorGrab {
|
||||||
|
fn motion(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut PointerInnerHandle<'_, State>,
|
||||||
|
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||||
|
event: &MotionEvent,
|
||||||
|
) {
|
||||||
|
handle.motion(data, None, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relative_motion(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut PointerInnerHandle<'_, State>,
|
||||||
|
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||||
|
event: &RelativeMotionEvent,
|
||||||
|
) {
|
||||||
|
handle.relative_motion(data, None, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn button(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut PointerInnerHandle<'_, State>,
|
||||||
|
event: &ButtonEvent,
|
||||||
|
) {
|
||||||
|
if event.state != ButtonState::Pressed {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're handling this press, don't send the release to the window.
|
||||||
|
data.niri.suppressed_buttons.insert(event.button);
|
||||||
|
|
||||||
|
if let Some(tx) = data.niri.pick_color.take() {
|
||||||
|
let color = Self::pick_color_at_point(handle.current_location(), data);
|
||||||
|
let _ = tx.send_blocking(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn axis(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut PointerInnerHandle<'_, State>,
|
||||||
|
details: AxisFrame,
|
||||||
|
) {
|
||||||
|
handle.axis(data, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||||
|
handle.frame(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gesture_swipe_begin(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut PointerInnerHandle<'_, State>,
|
||||||
|
event: &GestureSwipeBeginEvent,
|
||||||
|
) {
|
||||||
|
handle.gesture_swipe_begin(data, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gesture_swipe_update(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut PointerInnerHandle<'_, State>,
|
||||||
|
event: &GestureSwipeUpdateEvent,
|
||||||
|
) {
|
||||||
|
handle.gesture_swipe_update(data, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gesture_swipe_end(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut PointerInnerHandle<'_, State>,
|
||||||
|
event: &GestureSwipeEndEvent,
|
||||||
|
) {
|
||||||
|
handle.gesture_swipe_end(data, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gesture_pinch_begin(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut PointerInnerHandle<'_, State>,
|
||||||
|
event: &GesturePinchBeginEvent,
|
||||||
|
) {
|
||||||
|
handle.gesture_pinch_begin(data, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gesture_pinch_update(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut PointerInnerHandle<'_, State>,
|
||||||
|
event: &GesturePinchUpdateEvent,
|
||||||
|
) {
|
||||||
|
handle.gesture_pinch_update(data, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gesture_pinch_end(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut PointerInnerHandle<'_, State>,
|
||||||
|
event: &GesturePinchEndEvent,
|
||||||
|
) {
|
||||||
|
handle.gesture_pinch_end(data, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gesture_hold_begin(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut PointerInnerHandle<'_, State>,
|
||||||
|
event: &GestureHoldBeginEvent,
|
||||||
|
) {
|
||||||
|
handle.gesture_hold_begin(data, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gesture_hold_end(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut PointerInnerHandle<'_, State>,
|
||||||
|
event: &GestureHoldEndEvent,
|
||||||
|
) {
|
||||||
|
handle.gesture_hold_end(data, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_data(&self) -> &PointerGrabStartData<State> {
|
||||||
|
&self.start_data
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unset(&mut self, data: &mut State) {
|
||||||
|
self.on_ungrab(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
use smithay::backend::input::ButtonState;
|
||||||
|
use smithay::input::pointer::{
|
||||||
|
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||||
|
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||||
|
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
|
||||||
|
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||||
|
};
|
||||||
|
use smithay::input::SeatHandler;
|
||||||
|
use smithay::utils::{Logical, Point};
|
||||||
|
|
||||||
|
use crate::niri::State;
|
||||||
|
use crate::window::Mapped;
|
||||||
|
|
||||||
|
pub struct PickWindowGrab {
|
||||||
|
start_data: PointerGrabStartData<State>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickWindowGrab {
|
||||||
|
pub fn new(start_data: PointerGrabStartData<State>) -> Self {
|
||||||
|
Self { start_data }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_ungrab(&mut self, state: &mut State) {
|
||||||
|
if let Some(tx) = state.niri.pick_window.take() {
|
||||||
|
let _ = tx.send_blocking(None);
|
||||||
|
}
|
||||||
|
state
|
||||||
|
.niri
|
||||||
|
.cursor_manager
|
||||||
|
.set_cursor_image(CursorImageStatus::default_named());
|
||||||
|
// Redraw to update the cursor.
|
||||||
|
state.niri.queue_redraw_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerGrab<State> for PickWindowGrab {
|
||||||
|
fn motion(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut PointerInnerHandle<'_, State>,
|
||||||
|
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||||
|
event: &MotionEvent,
|
||||||
|
) {
|
||||||
|
handle.motion(data, None, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relative_motion(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut PointerInnerHandle<'_, State>,
|
||||||
|
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||||
|
event: &RelativeMotionEvent,
|
||||||
|
) {
|
||||||
|
handle.relative_motion(data, None, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn button(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut PointerInnerHandle<'_, State>,
|
||||||
|
event: &ButtonEvent,
|
||||||
|
) {
|
||||||
|
if event.state != ButtonState::Pressed {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're handling this press, don't send the release to the window.
|
||||||
|
data.niri.suppressed_buttons.insert(event.button);
|
||||||
|
|
||||||
|
if let Some(tx) = data.niri.pick_window.take() {
|
||||||
|
let _ = tx.send_blocking(
|
||||||
|
data.niri
|
||||||
|
.window_under(handle.current_location())
|
||||||
|
.map(Mapped::id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn axis(
|
||||||
|
&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,69 @@
|
|||||||
|
//! Swipe gesture from scroll events.
|
||||||
|
//!
|
||||||
|
//! Tracks when to begin, update, and end a swipe gesture from pointer axis events, also whether
|
||||||
|
//! the gesture is vertical or horizontal. Necessary because libinput only provides touchpad swipe
|
||||||
|
//! gesture events for 3+ fingers.
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ScrollSwipeGesture {
|
||||||
|
ongoing: bool,
|
||||||
|
vertical: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Action {
|
||||||
|
BeginUpdate,
|
||||||
|
Update,
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScrollSwipeGesture {
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
ongoing: false,
|
||||||
|
vertical: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, dx: f64, dy: f64) -> Action {
|
||||||
|
if dx == 0. && dy == 0. {
|
||||||
|
self.ongoing = false;
|
||||||
|
Action::End
|
||||||
|
} else if !self.ongoing {
|
||||||
|
self.ongoing = true;
|
||||||
|
self.vertical = dy != 0.;
|
||||||
|
Action::BeginUpdate
|
||||||
|
} else {
|
||||||
|
Action::Update
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) -> bool {
|
||||||
|
if self.ongoing {
|
||||||
|
self.ongoing = false;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_vertical(&self) -> bool {
|
||||||
|
self.vertical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ScrollSwipeGesture {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Action {
|
||||||
|
pub fn begin(self) -> bool {
|
||||||
|
self == Action::BeginUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end(self) -> bool {
|
||||||
|
self == Action::End
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,12 +10,14 @@ use smithay::input::SeatHandler;
|
|||||||
use smithay::output::Output;
|
use smithay::output::Output;
|
||||||
use smithay::utils::{Logical, Point};
|
use smithay::utils::{Logical, Point};
|
||||||
|
|
||||||
|
use crate::layout::workspace::WorkspaceId;
|
||||||
use crate::niri::State;
|
use crate::niri::State;
|
||||||
|
|
||||||
pub struct SpatialMovementGrab {
|
pub struct SpatialMovementGrab {
|
||||||
start_data: PointerGrabStartData<State>,
|
start_data: PointerGrabStartData<State>,
|
||||||
last_location: Point<f64, Logical>,
|
last_location: Point<f64, Logical>,
|
||||||
output: Output,
|
output: Output,
|
||||||
|
workspace_id: WorkspaceId,
|
||||||
gesture: GestureState,
|
gesture: GestureState,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,12 +29,24 @@ enum GestureState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SpatialMovementGrab {
|
impl SpatialMovementGrab {
|
||||||
pub fn new(start_data: PointerGrabStartData<State>, output: Output) -> Self {
|
pub fn new(
|
||||||
|
start_data: PointerGrabStartData<State>,
|
||||||
|
output: Output,
|
||||||
|
workspace_id: WorkspaceId,
|
||||||
|
is_view_offset: bool,
|
||||||
|
) -> Self {
|
||||||
|
let gesture = if is_view_offset {
|
||||||
|
GestureState::ViewOffset
|
||||||
|
} else {
|
||||||
|
GestureState::Recognizing
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
last_location: start_data.location,
|
last_location: start_data.location,
|
||||||
start_data,
|
start_data,
|
||||||
output,
|
output,
|
||||||
gesture: GestureState::Recognizing,
|
workspace_id,
|
||||||
|
gesture,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,10 +54,8 @@ impl SpatialMovementGrab {
|
|||||||
let layout = &mut state.niri.layout;
|
let layout = &mut state.niri.layout;
|
||||||
let res = match self.gesture {
|
let res = match self.gesture {
|
||||||
GestureState::Recognizing => None,
|
GestureState::Recognizing => None,
|
||||||
GestureState::ViewOffset => layout.view_offset_gesture_end(false, Some(false)),
|
GestureState::ViewOffset => layout.view_offset_gesture_end(Some(false)),
|
||||||
GestureState::WorkspaceSwitch => {
|
GestureState::WorkspaceSwitch => layout.workspace_switch_gesture_end(Some(false)),
|
||||||
layout.workspace_switch_gesture_end(false, Some(false))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(output) = res {
|
if let Some(output) = res {
|
||||||
@@ -81,8 +93,16 @@ impl PointerGrab<State> for SpatialMovementGrab {
|
|||||||
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
||||||
if c.x.abs() > c.y.abs() {
|
if c.x.abs() > c.y.abs() {
|
||||||
self.gesture = GestureState::ViewOffset;
|
self.gesture = GestureState::ViewOffset;
|
||||||
layout.view_offset_gesture_begin(&self.output, false);
|
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(self.workspace_id) {
|
||||||
layout.view_offset_gesture_update(-c.x, timestamp, false)
|
if ws.current_output() == Some(&self.output) {
|
||||||
|
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
|
||||||
|
layout.view_offset_gesture_update(-c.x, timestamp, false)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.gesture = GestureState::WorkspaceSwitch;
|
self.gesture = GestureState::WorkspaceSwitch;
|
||||||
layout.workspace_switch_gesture_begin(&self.output, false);
|
layout.workspace_switch_gesture_begin(&self.output, false);
|
||||||
@@ -105,7 +125,7 @@ impl PointerGrab<State> for SpatialMovementGrab {
|
|||||||
data.niri.queue_redraw(&output);
|
data.niri.queue_redraw(&output);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// The resize is no longer ongoing.
|
// The move is no longer ongoing.
|
||||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use smithay::desktop::Window;
|
||||||
|
use smithay::input::touch::{
|
||||||
|
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
|
||||||
|
TouchGrab, TouchInnerHandle, UpEvent,
|
||||||
|
};
|
||||||
|
use smithay::input::SeatHandler;
|
||||||
|
use smithay::output::Output;
|
||||||
|
use smithay::utils::{IsAlive, Logical, Point, Serial};
|
||||||
|
|
||||||
|
use crate::layout::workspace::{Workspace, WorkspaceId};
|
||||||
|
use crate::niri::State;
|
||||||
|
use crate::window::Mapped;
|
||||||
|
|
||||||
|
// When the touch is stationary for this much time, it becomes an interactive move.
|
||||||
|
const INTERACTIVE_MOVE_THRESHOLD: Duration = Duration::from_millis(500);
|
||||||
|
|
||||||
|
pub struct TouchOverviewGrab {
|
||||||
|
start_data: TouchGrabStartData<State>,
|
||||||
|
start_timestamp: Duration,
|
||||||
|
last_location: Point<f64, Logical>,
|
||||||
|
output: Output,
|
||||||
|
start_pos_within_output: Point<f64, Logical>,
|
||||||
|
workspace_id: Option<WorkspaceId>,
|
||||||
|
workspace_matched_narrow: bool,
|
||||||
|
window: Option<Window>,
|
||||||
|
gesture: GestureState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum GestureState {
|
||||||
|
Recognizing,
|
||||||
|
ViewOffset,
|
||||||
|
WorkspaceSwitch,
|
||||||
|
InteractiveMove,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TouchOverviewGrab {
|
||||||
|
pub fn new(
|
||||||
|
start_data: TouchGrabStartData<State>,
|
||||||
|
start_timestamp: Duration,
|
||||||
|
output: Output,
|
||||||
|
start_pos_within_output: Point<f64, Logical>,
|
||||||
|
workspace_id: Option<WorkspaceId>,
|
||||||
|
workspace_matched_narrow: bool,
|
||||||
|
window: Option<Window>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
last_location: start_data.location,
|
||||||
|
start_timestamp,
|
||||||
|
start_data,
|
||||||
|
output,
|
||||||
|
start_pos_within_output,
|
||||||
|
workspace_id,
|
||||||
|
workspace_matched_narrow,
|
||||||
|
window,
|
||||||
|
gesture: GestureState::Recognizing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_ungrab(&mut self, state: &mut State) {
|
||||||
|
let layout = &mut state.niri.layout;
|
||||||
|
match self.gesture {
|
||||||
|
GestureState::Recognizing => {
|
||||||
|
// Tap to activate.
|
||||||
|
layout.focus_output(&self.output);
|
||||||
|
|
||||||
|
// Activate the workspace if necessary.
|
||||||
|
if self.window.is_some() || self.workspace_matched_narrow {
|
||||||
|
// When activating a window, we want to activate the window's current
|
||||||
|
// workspace. Otherwise, find the workspace that we tapped on.
|
||||||
|
let ws_matches = |ws: &Workspace<Mapped>| {
|
||||||
|
if let Some(window) = &self.window {
|
||||||
|
ws.has_window(window)
|
||||||
|
} else if let Some(ws_id) = self.workspace_id {
|
||||||
|
ws.id() == ws_id
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let ws_idx = if let Some((Some(mon), ws_idx, _)) =
|
||||||
|
layout.workspaces().find(|(_, _, ws)| ws_matches(ws))
|
||||||
|
{
|
||||||
|
// The workspace could've moved to a different output in the meantime.
|
||||||
|
(*mon.output() == self.output).then_some(ws_idx)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ws_idx) = ws_idx {
|
||||||
|
layout.toggle_overview_to_workspace(ws_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(window) = self.window.as_ref() {
|
||||||
|
layout.activate_window(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GestureState::ViewOffset => {
|
||||||
|
layout.view_offset_gesture_end(Some(false));
|
||||||
|
}
|
||||||
|
GestureState::WorkspaceSwitch => {
|
||||||
|
layout.workspace_switch_gesture_end(Some(false));
|
||||||
|
}
|
||||||
|
GestureState::InteractiveMove => {
|
||||||
|
layout.interactive_move_end(self.window.as_ref().unwrap());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
state.niri.queue_redraw_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TouchGrab<State> for TouchOverviewGrab {
|
||||||
|
fn down(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut TouchInnerHandle<'_, State>,
|
||||||
|
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||||
|
event: &DownEvent,
|
||||||
|
seq: Serial,
|
||||||
|
) {
|
||||||
|
handle.down(data, None, event, seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn up(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut TouchInnerHandle<'_, State>,
|
||||||
|
event: &UpEvent,
|
||||||
|
seq: Serial,
|
||||||
|
) {
|
||||||
|
handle.up(data, event, seq);
|
||||||
|
|
||||||
|
if event.slot != self.start_data.slot {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.unset_grab(self, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn motion(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut TouchInnerHandle<'_, State>,
|
||||||
|
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||||
|
event: &MotionEvent,
|
||||||
|
seq: Serial,
|
||||||
|
) {
|
||||||
|
handle.motion(data, None, event, seq);
|
||||||
|
|
||||||
|
if event.slot != self.start_data.slot {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||||
|
let layout = &mut data.niri.layout;
|
||||||
|
|
||||||
|
// Check if we should become interactive move.
|
||||||
|
if matches!(self.gesture, GestureState::Recognizing) {
|
||||||
|
if let Some(window) = self.window.as_ref().filter(|win| win.alive()) {
|
||||||
|
let passed = timestamp.saturating_sub(self.start_timestamp);
|
||||||
|
if INTERACTIVE_MOVE_THRESHOLD <= passed
|
||||||
|
&& layout.interactive_move_begin(
|
||||||
|
window.clone(),
|
||||||
|
&self.output,
|
||||||
|
self.start_pos_within_output,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
self.gesture = GestureState::InteractiveMove;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should become a spatial scroll.
|
||||||
|
if matches!(self.gesture, GestureState::Recognizing) {
|
||||||
|
let c = event.location - self.start_data.location;
|
||||||
|
|
||||||
|
// Check if the gesture moved far enough to decide. Threshold copied from libadwaita.
|
||||||
|
if c.x * c.x + c.y * c.y >= 16. * 16. {
|
||||||
|
if let Some(ws_id) = self.workspace_id.filter(|_| c.x.abs() > c.y.abs()) {
|
||||||
|
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(ws_id) {
|
||||||
|
if ws.current_output() == Some(&self.output) {
|
||||||
|
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
|
||||||
|
self.gesture = GestureState::ViewOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(self.gesture, GestureState::Recognizing) {
|
||||||
|
layout.workspace_switch_gesture_begin(&self.output, false);
|
||||||
|
self.gesture = GestureState::WorkspaceSwitch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do nothing if still recognizing.
|
||||||
|
if matches!(self.gesture, GestureState::Recognizing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let delta = event.location - self.last_location;
|
||||||
|
self.last_location = event.location;
|
||||||
|
|
||||||
|
let ongoing = match self.gesture {
|
||||||
|
GestureState::Recognizing => unreachable!(),
|
||||||
|
GestureState::ViewOffset => layout
|
||||||
|
.view_offset_gesture_update(-delta.x, timestamp, false)
|
||||||
|
.is_some(),
|
||||||
|
GestureState::WorkspaceSwitch => layout
|
||||||
|
.workspace_switch_gesture_update(-delta.y, timestamp, false)
|
||||||
|
.is_some(),
|
||||||
|
GestureState::InteractiveMove => {
|
||||||
|
let window = self.window.as_ref().unwrap();
|
||||||
|
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
|
||||||
|
let output = output.clone();
|
||||||
|
data.niri.layout.interactive_move_update(
|
||||||
|
window,
|
||||||
|
delta,
|
||||||
|
output,
|
||||||
|
pos_within_output,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ongoing {
|
||||||
|
data.niri.queue_redraw_all();
|
||||||
|
} else {
|
||||||
|
handle.unset_grab(self, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||||
|
handle.frame(data, seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||||
|
handle.cancel(data, seq);
|
||||||
|
handle.unset_grab(self, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut TouchInnerHandle<'_, State>,
|
||||||
|
event: &ShapeEvent,
|
||||||
|
seq: Serial,
|
||||||
|
) {
|
||||||
|
handle.shape(data, event, seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn orientation(
|
||||||
|
&mut self,
|
||||||
|
data: &mut State,
|
||||||
|
handle: &mut TouchInnerHandle<'_, State>,
|
||||||
|
event: &OrientationEvent,
|
||||||
|
seq: Serial,
|
||||||
|
) {
|
||||||
|
handle.orientation(data, event, seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_data(&self) -> &TouchGrabStartData<State> {
|
||||||
|
&self.start_data
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unset(&mut self, data: &mut State) {
|
||||||
|
self.on_ungrab(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
+195
-37
@@ -1,9 +1,13 @@
|
|||||||
|
use std::io::ErrorKind;
|
||||||
|
use std::iter::Peekable;
|
||||||
|
use std::slice;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Context};
|
use anyhow::{anyhow, bail, Context};
|
||||||
use niri_config::OutputName;
|
use niri_config::OutputName;
|
||||||
use niri_ipc::socket::Socket;
|
use niri_ipc::socket::Socket;
|
||||||
use niri_ipc::{
|
use niri_ipc::{
|
||||||
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response,
|
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Overview, Request,
|
||||||
Transform, Window,
|
Response, Transform, Window,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
@@ -16,6 +20,8 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
|||||||
Msg::Outputs => Request::Outputs,
|
Msg::Outputs => Request::Outputs,
|
||||||
Msg::FocusedWindow => Request::FocusedWindow,
|
Msg::FocusedWindow => Request::FocusedWindow,
|
||||||
Msg::FocusedOutput => Request::FocusedOutput,
|
Msg::FocusedOutput => Request::FocusedOutput,
|
||||||
|
Msg::PickWindow => Request::PickWindow,
|
||||||
|
Msg::PickColor => Request::PickColor,
|
||||||
Msg::Action { action } => Request::Action(action.clone()),
|
Msg::Action { action } => Request::Action(action.clone()),
|
||||||
Msg::Output { output, action } => Request::Output {
|
Msg::Output { output, action } => Request::Output {
|
||||||
output: output.clone(),
|
output: output.clone(),
|
||||||
@@ -23,27 +29,39 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
|||||||
},
|
},
|
||||||
Msg::Workspaces => Request::Workspaces,
|
Msg::Workspaces => Request::Workspaces,
|
||||||
Msg::Windows => Request::Windows,
|
Msg::Windows => Request::Windows,
|
||||||
|
Msg::Layers => Request::Layers,
|
||||||
Msg::KeyboardLayouts => Request::KeyboardLayouts,
|
Msg::KeyboardLayouts => Request::KeyboardLayouts,
|
||||||
Msg::EventStream => Request::EventStream,
|
Msg::EventStream => Request::EventStream,
|
||||||
Msg::RequestError => Request::ReturnError,
|
Msg::RequestError => Request::ReturnError,
|
||||||
|
Msg::OverviewState => Request::OverviewState,
|
||||||
};
|
};
|
||||||
|
|
||||||
let socket = Socket::connect().context("error connecting to the niri socket")?;
|
let mut socket = Socket::connect().context("error connecting to the niri socket")?;
|
||||||
|
|
||||||
let (reply, mut read_event) = socket
|
let result = socket.send(request);
|
||||||
.send(request)
|
|
||||||
.context("error communicating with niri")?;
|
|
||||||
|
|
||||||
let compositor_version = match reply {
|
// For errors that can be caused by a version mismatch between the running niri instance and
|
||||||
Err(_) if !matches!(msg, Msg::Version) => {
|
// the niri msg CLI, we will try to fetch and compare the versions.
|
||||||
// If we got an error, it might be that the CLI is a different version from the running
|
let check_compositor_version = match &result {
|
||||||
// niri instance. Request the running instance version to compare and print a message.
|
Err(err) => {
|
||||||
Socket::connect()
|
// Response JSON parsing errors.
|
||||||
.and_then(|socket| socket.send(Request::Version))
|
matches!(
|
||||||
.ok()
|
err.kind(),
|
||||||
.map(|(reply, _read_event)| reply)
|
ErrorKind::InvalidData | ErrorKind::UnexpectedEof
|
||||||
|
)
|
||||||
}
|
}
|
||||||
_ => None,
|
// Error returned from niri.
|
||||||
|
Ok(Err(_)) => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let compositor_version = if check_compositor_version && !matches!(msg, Msg::Version) {
|
||||||
|
// Reconnect to support older niri versions with one request per connection.
|
||||||
|
Socket::connect()
|
||||||
|
.and_then(|mut socket| socket.send(Request::Version))
|
||||||
|
.ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default SIGPIPE so that our prints don't panic on stdout closing.
|
// Default SIGPIPE so that our prints don't panic on stdout closing.
|
||||||
@@ -51,32 +69,31 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
|||||||
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = reply.map_err(|err_msg| {
|
// Check for CLI-server version mismatch to add helpful context.
|
||||||
// Check for CLI-server version mismatch to add helpful context.
|
match compositor_version {
|
||||||
match compositor_version {
|
Some(Ok(Response::Version(compositor_version))) => {
|
||||||
Some(Ok(Response::Version(compositor_version))) => {
|
let cli_version = version();
|
||||||
let cli_version = version();
|
if cli_version != compositor_version {
|
||||||
if cli_version != compositor_version {
|
eprintln!("Running niri compositor has a different version from the niri CLI:");
|
||||||
eprintln!("Running niri compositor has a different version from the niri CLI:");
|
eprintln!("Compositor version: {compositor_version}");
|
||||||
eprintln!("Compositor version: {compositor_version}");
|
eprintln!("CLI version: {cli_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!("Did you forget to restart niri after an update?");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
}
|
}
|
||||||
None => {
|
|
||||||
// Communication error, or the original request was already a version request.
|
|
||||||
// Don't add irrelevant context.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Some(_) => {
|
||||||
|
eprintln!("Unable to get the running niri compositor version.");
|
||||||
|
eprintln!("Did you forget to restart niri after an update?");
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Communication error, or the original request was already a version request, or the
|
||||||
|
// original request had succeeded. Don't add irrelevant context.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
anyhow!(err_msg).context("niri returned an error")
|
let reply = result.context("error communicating with niri")?;
|
||||||
})?;
|
let response = reply.map_err(|err_msg| anyhow!(err_msg).context("niri returned an error"))?;
|
||||||
|
|
||||||
match msg {
|
match msg {
|
||||||
Msg::RequestError => {
|
Msg::RequestError => {
|
||||||
@@ -168,6 +185,69 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
|||||||
println!();
|
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 => {
|
Msg::FocusedOutput => {
|
||||||
let Response::FocusedOutput(output) = response else {
|
let Response::FocusedOutput(output) = response else {
|
||||||
bail!("unexpected response: expected FocusedOutput, got {response:?}");
|
bail!("unexpected response: expected FocusedOutput, got {response:?}");
|
||||||
@@ -185,6 +265,43 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
|||||||
println!("No output is focused.");
|
println!("No output is focused.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Msg::PickWindow => {
|
||||||
|
let Response::PickedWindow(window) = response else {
|
||||||
|
bail!("unexpected response: expected PickedWindow, got {response:?}");
|
||||||
|
};
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let window = serde_json::to_string(&window).context("error formatting response")?;
|
||||||
|
println!("{window}");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(window) = window {
|
||||||
|
print_window(&window);
|
||||||
|
} else {
|
||||||
|
println!("No window selected.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Msg::PickColor => {
|
||||||
|
let Response::PickedColor(color) = response else {
|
||||||
|
bail!("unexpected response: expected PickedColor, got {response:?}");
|
||||||
|
};
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let color = serde_json::to_string(&color).context("error formatting response")?;
|
||||||
|
println!("{color}");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(color) = color {
|
||||||
|
let [r, g, b] = color.rgb.map(|v| (v.clamp(0., 1.) * 255.).round() as u8);
|
||||||
|
|
||||||
|
println!("Picked color: rgb({r}, {g}, {b})",);
|
||||||
|
println!("Hex: #{:02x}{:02x}{:02x}", r, g, b);
|
||||||
|
} else {
|
||||||
|
println!("No color was picked.");
|
||||||
|
}
|
||||||
|
}
|
||||||
Msg::Action { .. } => {
|
Msg::Action { .. } => {
|
||||||
let Response::Handled = response else {
|
let Response::Handled = response else {
|
||||||
bail!("unexpected response: expected Handled, got {response:?}");
|
bail!("unexpected response: expected Handled, got {response:?}");
|
||||||
@@ -285,6 +402,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
|||||||
println!("Started reading events.");
|
println!("Started reading events.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut read_event = socket.read_events();
|
||||||
loop {
|
loop {
|
||||||
let event = read_event().context("error reading event from niri")?;
|
let event = read_event().context("error reading event from niri")?;
|
||||||
|
|
||||||
@@ -298,6 +416,9 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
|||||||
Event::WorkspacesChanged { workspaces } => {
|
Event::WorkspacesChanged { workspaces } => {
|
||||||
println!("Workspaces changed: {workspaces:?}");
|
println!("Workspaces changed: {workspaces:?}");
|
||||||
}
|
}
|
||||||
|
Event::WorkspaceUrgencyChanged { id, urgent } => {
|
||||||
|
println!("Workspace {id}: urgency changed to {urgent}");
|
||||||
|
}
|
||||||
Event::WorkspaceActivated { id, focused } => {
|
Event::WorkspaceActivated { id, focused } => {
|
||||||
let word = if focused { "focused" } else { "activated" };
|
let word = if focused { "focused" } else { "activated" };
|
||||||
println!("Workspace {word}: {id}");
|
println!("Workspace {word}: {id}");
|
||||||
@@ -323,15 +444,40 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
|||||||
Event::WindowFocusChanged { id } => {
|
Event::WindowFocusChanged { id } => {
|
||||||
println!("Window focus changed: {id:?}");
|
println!("Window focus changed: {id:?}");
|
||||||
}
|
}
|
||||||
|
Event::WindowUrgencyChanged { id, urgent } => {
|
||||||
|
println!("Window {id}: urgency changed to {urgent}");
|
||||||
|
}
|
||||||
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
|
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
|
||||||
println!("Keyboard layouts changed: {keyboard_layouts:?}");
|
println!("Keyboard layouts changed: {keyboard_layouts:?}");
|
||||||
}
|
}
|
||||||
Event::KeyboardLayoutSwitched { idx } => {
|
Event::KeyboardLayoutSwitched { idx } => {
|
||||||
println!("Keyboard layout switched: {idx}");
|
println!("Keyboard layout switched: {idx}");
|
||||||
}
|
}
|
||||||
|
Event::OverviewOpenedOrClosed { is_open: opened } => {
|
||||||
|
println!("Overview toggled: {opened}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Msg::OverviewState => {
|
||||||
|
let Response::OverviewState(response) = response else {
|
||||||
|
bail!("unexpected response: expected Overview, got {response:?}");
|
||||||
|
};
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let response =
|
||||||
|
serde_json::to_string(&response).context("error formatting response")?;
|
||||||
|
println!("{response}");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Overview { is_open } = response;
|
||||||
|
if is_open {
|
||||||
|
println!("Overview is open.");
|
||||||
|
} else {
|
||||||
|
println!("Overview is closed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -435,7 +581,8 @@ fn print_output(output: Output) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
fn print_window(window: &Window) {
|
fn print_window(window: &Window) {
|
||||||
let focused = if window.is_focused { " (focused)" } else { "" };
|
let focused = if window.is_focused { " (focused)" } else { "" };
|
||||||
println!("Window ID {}:{focused}", window.id);
|
let urgent = if window.is_urgent { " (urgent)" } else { "" };
|
||||||
|
println!("Window ID {}:{focused}{urgent}", window.id);
|
||||||
|
|
||||||
if let Some(title) = &window.title {
|
if let Some(title) = &window.title {
|
||||||
println!(" Title: \"{title}\"");
|
println!(" Title: \"{title}\"");
|
||||||
@@ -449,6 +596,17 @@ fn print_window(window: &Window) {
|
|||||||
println!(" App ID: (unset)");
|
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 {
|
if let Some(workspace_id) = window.workspace_id {
|
||||||
println!(" Workspace ID: {workspace_id}");
|
println!(" Workspace ID: {workspace_id}");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+242
-88
@@ -1,5 +1,6 @@
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::ffi::OsStr;
|
||||||
use std::os::unix::net::{UnixListener, UnixStream};
|
use std::os::unix::net::{UnixListener, UnixStream};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
@@ -15,12 +16,21 @@ use futures_util::io::{AsyncReadExt, BufReader};
|
|||||||
use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, FutureExt as _};
|
use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, FutureExt as _};
|
||||||
use niri_config::OutputName;
|
use niri_config::OutputName;
|
||||||
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
|
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
|
||||||
use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace};
|
use niri_ipc::{
|
||||||
|
Event, KeyboardLayouts, OutputConfigChanged, Overview, Reply, Request, Response, Workspace,
|
||||||
|
};
|
||||||
|
use smithay::desktop::layer_map_for_output;
|
||||||
|
use smithay::input::pointer::{
|
||||||
|
CursorIcon, CursorImageStatus, Focus, GrabStartData as PointerGrabStartData,
|
||||||
|
};
|
||||||
use smithay::reexports::calloop::generic::Generic;
|
use smithay::reexports::calloop::generic::Generic;
|
||||||
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||||
use smithay::reexports::rustix::fs::unlink;
|
use smithay::reexports::rustix::fs::unlink;
|
||||||
|
use smithay::utils::SERIAL_COUNTER;
|
||||||
|
use smithay::wayland::shell::wlr_layer::{KeyboardInteractivity, Layer};
|
||||||
|
|
||||||
use crate::backend::IpcOutputMap;
|
use crate::backend::IpcOutputMap;
|
||||||
|
use crate::input::pick_window_grab::PickWindowGrab;
|
||||||
use crate::layout::workspace::WorkspaceId;
|
use crate::layout::workspace::WorkspaceId;
|
||||||
use crate::niri::State;
|
use crate::niri::State;
|
||||||
use crate::utils::{version, with_toplevel_role};
|
use crate::utils::{version, with_toplevel_role};
|
||||||
@@ -31,7 +41,10 @@ use crate::window::Mapped;
|
|||||||
const EVENT_STREAM_BUFFER_SIZE: usize = 64;
|
const EVENT_STREAM_BUFFER_SIZE: usize = 64;
|
||||||
|
|
||||||
pub struct IpcServer {
|
pub struct IpcServer {
|
||||||
pub socket_path: PathBuf,
|
/// Path to the IPC socket.
|
||||||
|
///
|
||||||
|
/// This is `None` when creating `IpcServer` without a socket.
|
||||||
|
pub socket_path: Option<PathBuf>,
|
||||||
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
|
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
|
||||||
event_stream_state: Rc<RefCell<EventStreamState>>,
|
event_stream_state: Rc<RefCell<EventStreamState>>,
|
||||||
}
|
}
|
||||||
@@ -58,31 +71,38 @@ struct EventStreamSender {
|
|||||||
impl IpcServer {
|
impl IpcServer {
|
||||||
pub fn start(
|
pub fn start(
|
||||||
event_loop: &LoopHandle<'static, State>,
|
event_loop: &LoopHandle<'static, State>,
|
||||||
wayland_socket_name: &str,
|
wayland_socket_name: Option<&OsStr>,
|
||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
let _span = tracy_client::span!("Ipc::start");
|
let _span = tracy_client::span!("Ipc::start");
|
||||||
|
|
||||||
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
|
let socket_path = if let Some(wayland_socket_name) = wayland_socket_name {
|
||||||
let mut socket_path = socket_dir();
|
let wayland_socket_name = wayland_socket_name.to_string_lossy();
|
||||||
socket_path.push(socket_name);
|
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
|
||||||
|
let mut socket_path = socket_dir();
|
||||||
|
socket_path.push(socket_name);
|
||||||
|
|
||||||
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
|
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
|
||||||
listener
|
listener
|
||||||
.set_nonblocking(true)
|
.set_nonblocking(true)
|
||||||
.context("error setting socket to non-blocking")?;
|
.context("error setting socket to non-blocking")?;
|
||||||
|
|
||||||
let source = Generic::new(listener, Interest::READ, Mode::Level);
|
let source = Generic::new(listener, Interest::READ, Mode::Level);
|
||||||
event_loop
|
event_loop
|
||||||
.insert_source(source, |_, socket, state| {
|
.insert_source(source, |_, socket, state| {
|
||||||
match socket.accept() {
|
match socket.accept() {
|
||||||
Ok((stream, _)) => on_new_ipc_client(state, stream),
|
Ok((stream, _)) => on_new_ipc_client(state, stream),
|
||||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
|
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(PostAction::Continue)
|
Ok(PostAction::Continue)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
Some(socket_path)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
socket_path,
|
socket_path,
|
||||||
@@ -117,7 +137,9 @@ impl IpcServer {
|
|||||||
|
|
||||||
impl Drop for IpcServer {
|
impl Drop for IpcServer {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let _ = unlink(&self.socket_path);
|
if let Some(socket_path) = &self.socket_path {
|
||||||
|
let _ = unlink(socket_path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,76 +185,86 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
|
|||||||
|
|
||||||
async fn handle_client(ctx: ClientCtx, stream: Async<'static, UnixStream>) -> anyhow::Result<()> {
|
async fn handle_client(ctx: ClientCtx, stream: Async<'static, UnixStream>) -> anyhow::Result<()> {
|
||||||
let (read, mut write) = stream.split();
|
let (read, mut write) = stream.split();
|
||||||
let mut buf = String::new();
|
let mut read = BufReader::new(read);
|
||||||
|
|
||||||
// Read a single line to allow extensibility in the future to keep reading.
|
loop {
|
||||||
BufReader::new(read)
|
// Don't keep buf around to avoid clients wasting RAM by filling it with bogus data.
|
||||||
.read_line(&mut buf)
|
let mut buf = Vec::new();
|
||||||
.await
|
let res = read.read_until(b'\n', &mut buf).await;
|
||||||
.context("error reading request")?;
|
match res {
|
||||||
|
Ok(0) => return Ok(()),
|
||||||
let request = serde_json::from_str(&buf)
|
Ok(_) => (),
|
||||||
.context("error parsing request")
|
// Normal client disconnection.
|
||||||
.map_err(|err| err.to_string());
|
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
|
||||||
let requested_error = matches!(request, Ok(Request::ReturnError));
|
Err(err) => {
|
||||||
let requested_event_stream = matches!(request, Ok(Request::EventStream));
|
return Err(err).context("error reading request");
|
||||||
|
|
||||||
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 request = serde_json::from_slice(&buf)
|
||||||
{
|
.context("error parsing request")
|
||||||
let mut streams = ctx.event_streams.borrow_mut();
|
.map_err(|err| err.to_string());
|
||||||
let sender = EventStreamSender {
|
let requested_error = matches!(request, Ok(Request::ReturnError));
|
||||||
events: events_tx,
|
let requested_event_stream = matches!(request, Ok(Request::EventStream));
|
||||||
disconnect: disconnect_tx,
|
|
||||||
|
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:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.clear();
|
||||||
|
serde_json::to_writer(&mut buf, &reply).context("error formatting reply")?;
|
||||||
|
buf.push(b'\n');
|
||||||
|
write.write_all(&buf).await.context("error writing reply")?;
|
||||||
|
|
||||||
|
if requested_event_stream {
|
||||||
|
let (events_tx, events_rx) = async_channel::bounded(EVENT_STREAM_BUFFER_SIZE);
|
||||||
|
let (disconnect_tx, disconnect_rx) = async_channel::bounded(1);
|
||||||
|
|
||||||
|
// Spawn a task for the client.
|
||||||
|
let client = EventStreamClient {
|
||||||
|
events: events_rx,
|
||||||
|
disconnect: disconnect_rx,
|
||||||
|
write: Box::new(write) as _,
|
||||||
};
|
};
|
||||||
streams.push(sender);
|
let future = async move {
|
||||||
|
if let Err(err) = handle_event_stream_client(client).await {
|
||||||
|
warn!("error handling IPC event stream client: {err:?}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(err) = ctx.scheduler.schedule(future) {
|
||||||
|
warn!("error scheduling IPC event stream future: {err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the initial state.
|
||||||
|
{
|
||||||
|
let state = ctx.event_stream_state.borrow();
|
||||||
|
for event in state.replicate() {
|
||||||
|
events_tx
|
||||||
|
.try_send(event)
|
||||||
|
.expect("initial event burst had more events than buffer size");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add it to the list.
|
||||||
|
{
|
||||||
|
let mut streams = ctx.event_streams.borrow_mut();
|
||||||
|
let sender = EventStreamSender {
|
||||||
|
events: events_tx,
|
||||||
|
disconnect: disconnect_tx,
|
||||||
|
};
|
||||||
|
streams.push(sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||||
@@ -254,6 +286,47 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
|||||||
let windows = state.windows.windows.values().cloned().collect();
|
let windows = state.windows.windows.values().cloned().collect();
|
||||||
Response::Windows(windows)
|
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 => {
|
Request::KeyboardLayouts => {
|
||||||
let state = ctx.event_stream_state.borrow();
|
let state = ctx.event_stream_state.borrow();
|
||||||
let layout = state.keyboard_layouts.keyboard_layouts.clone();
|
let layout = state.keyboard_layouts.keyboard_layouts.clone();
|
||||||
@@ -266,11 +339,52 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
|||||||
let window = windows.values().find(|win| win.is_focused).cloned();
|
let window = windows.values().find(|win| win.is_focused).cloned();
|
||||||
Response::FocusedWindow(window)
|
Response::FocusedWindow(window)
|
||||||
}
|
}
|
||||||
|
Request::PickWindow => {
|
||||||
|
let (tx, rx) = async_channel::bounded(1);
|
||||||
|
ctx.event_loop.insert_idle(move |state| {
|
||||||
|
let pointer = state.niri.seat.get_pointer().unwrap();
|
||||||
|
let start_data = PointerGrabStartData {
|
||||||
|
focus: None,
|
||||||
|
button: 0,
|
||||||
|
location: pointer.current_location(),
|
||||||
|
};
|
||||||
|
let grab = PickWindowGrab::new(start_data);
|
||||||
|
// The `WindowPickGrab` ungrab handler will cancel the previous ongoing pick, if
|
||||||
|
// any.
|
||||||
|
pointer.set_grab(state, grab, SERIAL_COUNTER.next_serial(), Focus::Clear);
|
||||||
|
state.niri.pick_window = Some(tx);
|
||||||
|
state
|
||||||
|
.niri
|
||||||
|
.cursor_manager
|
||||||
|
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Crosshair));
|
||||||
|
// Redraw to update the cursor.
|
||||||
|
state.niri.queue_redraw_all();
|
||||||
|
});
|
||||||
|
let result = rx.recv().await;
|
||||||
|
let id = result.map_err(|_| String::from("error getting picked window info"))?;
|
||||||
|
let window = id.and_then(|id| {
|
||||||
|
let state = ctx.event_stream_state.borrow();
|
||||||
|
state.windows.windows.get(&id.get()).cloned()
|
||||||
|
});
|
||||||
|
Response::PickedWindow(window)
|
||||||
|
}
|
||||||
|
Request::PickColor => {
|
||||||
|
let (tx, rx) = async_channel::bounded(1);
|
||||||
|
ctx.event_loop.insert_idle(move |state| {
|
||||||
|
state.handle_pick_color(tx);
|
||||||
|
});
|
||||||
|
let result = rx.recv().await;
|
||||||
|
let color = result.map_err(|_| String::from("error getting picked color"))?;
|
||||||
|
Response::PickedColor(color)
|
||||||
|
}
|
||||||
Request::Action(action) => {
|
Request::Action(action) => {
|
||||||
let (tx, rx) = async_channel::bounded(1);
|
let (tx, rx) = async_channel::bounded(1);
|
||||||
|
|
||||||
let action = niri_config::Action::from(action);
|
let action = niri_config::Action::from(action);
|
||||||
ctx.event_loop.insert_idle(move |state| {
|
ctx.event_loop.insert_idle(move |state| {
|
||||||
|
// Make sure some logic like workspace clean-up has a chance to run before doing
|
||||||
|
// actions.
|
||||||
|
state.niri.advance_animations();
|
||||||
state.do_action(action, false);
|
state.do_action(action, false);
|
||||||
let _ = tx.send_blocking(());
|
let _ = tx.send_blocking(());
|
||||||
});
|
});
|
||||||
@@ -326,6 +440,11 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
|||||||
Response::FocusedOutput(output)
|
Response::FocusedOutput(output)
|
||||||
}
|
}
|
||||||
Request::EventStream => Response::Handled,
|
Request::EventStream => Response::Handled,
|
||||||
|
Request::OverviewState => {
|
||||||
|
let state = ctx.event_stream_state.borrow();
|
||||||
|
let is_open = state.overview.is_open;
|
||||||
|
Response::OverviewState(Overview { is_open })
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
@@ -363,8 +482,11 @@ fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_i
|
|||||||
id: mapped.id().get(),
|
id: mapped.id().get(),
|
||||||
title: role.title.clone(),
|
title: role.title.clone(),
|
||||||
app_id: role.app_id.clone(),
|
app_id: role.app_id.clone(),
|
||||||
|
pid: mapped.credentials().map(|c| c.pid),
|
||||||
workspace_id: workspace_id.map(|id| id.get()),
|
workspace_id: workspace_id.map(|id| id.get()),
|
||||||
is_focused: mapped.is_focused(),
|
is_focused: mapped.is_focused(),
|
||||||
|
is_floating: mapped.is_floating(),
|
||||||
|
is_urgent: mapped.is_urgent(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,6 +542,7 @@ impl State {
|
|||||||
pub fn ipc_refresh_layout(&mut self) {
|
pub fn ipc_refresh_layout(&mut self) {
|
||||||
self.ipc_refresh_workspaces();
|
self.ipc_refresh_workspaces();
|
||||||
self.ipc_refresh_windows();
|
self.ipc_refresh_windows();
|
||||||
|
self.ipc_refresh_overview();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ipc_refresh_workspaces(&mut self) {
|
fn ipc_refresh_workspaces(&mut self) {
|
||||||
@@ -467,6 +590,12 @@ impl State {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this workspace urgent state changed.
|
||||||
|
let urgent = ws.is_urgent();
|
||||||
|
if urgent != ipc_ws.is_urgent {
|
||||||
|
events.push(Event::WorkspaceUrgencyChanged { id, urgent });
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this workspace became focused.
|
// Check if this workspace became focused.
|
||||||
let is_focused = Some(id) == focused_ws_id;
|
let is_focused = Some(id) == focused_ws_id;
|
||||||
if is_focused && !ipc_ws.is_focused {
|
if is_focused && !ipc_ws.is_focused {
|
||||||
@@ -475,7 +604,7 @@ impl State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if this workspace became active.
|
// Check if this workspace became active.
|
||||||
let is_active = mon.map_or(false, |mon| mon.active_workspace_idx() == ws_idx);
|
let is_active = mon.is_some_and(|mon| mon.active_workspace_idx() == ws_idx);
|
||||||
if is_active && !ipc_ws.is_active {
|
if is_active && !ipc_ws.is_active {
|
||||||
events.push(Event::WorkspaceActivated { id, focused: false });
|
events.push(Event::WorkspaceActivated { id, focused: false });
|
||||||
}
|
}
|
||||||
@@ -498,7 +627,8 @@ impl State {
|
|||||||
idx: u8::try_from(ws_idx + 1).unwrap_or(u8::MAX),
|
idx: u8::try_from(ws_idx + 1).unwrap_or(u8::MAX),
|
||||||
name: ws.name().cloned(),
|
name: ws.name().cloned(),
|
||||||
output: mon.map(|mon| mon.output_name().clone()),
|
output: mon.map(|mon| mon.output_name().clone()),
|
||||||
is_active: mon.map_or(false, |mon| mon.active_workspace_idx() == ws_idx),
|
is_urgent: ws.is_urgent(),
|
||||||
|
is_active: mon.is_some_and(|mon| mon.active_workspace_idx() == ws_idx),
|
||||||
is_focused: Some(id) == focused_ws_id,
|
is_focused: Some(id) == focused_ws_id,
|
||||||
active_window_id: ws.active_window().map(|win| win.id().get()),
|
active_window_id: ws.active_window().map(|win| win.id().get()),
|
||||||
}
|
}
|
||||||
@@ -545,7 +675,8 @@ impl State {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let workspace_id = ws_id.map(|id| id.get());
|
let workspace_id = ws_id.map(|id| id.get());
|
||||||
let mut changed = ipc_win.workspace_id != workspace_id;
|
let mut changed =
|
||||||
|
ipc_win.workspace_id != workspace_id || ipc_win.is_floating != mapped.is_floating();
|
||||||
|
|
||||||
changed |= with_toplevel_role(mapped.toplevel(), |role| {
|
changed |= with_toplevel_role(mapped.toplevel(), |role| {
|
||||||
ipc_win.title != role.title || ipc_win.app_id != role.app_id
|
ipc_win.title != role.title || ipc_win.app_id != role.app_id
|
||||||
@@ -560,6 +691,11 @@ impl State {
|
|||||||
if mapped.is_focused() && !ipc_win.is_focused {
|
if mapped.is_focused() && !ipc_win.is_focused {
|
||||||
events.push(Event::WindowFocusChanged { id: Some(id) });
|
events.push(Event::WindowFocusChanged { id: Some(id) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let urgent = mapped.is_urgent();
|
||||||
|
if urgent != ipc_win.is_urgent {
|
||||||
|
events.push(Event::WindowUrgencyChanged { id, urgent })
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check for closed windows.
|
// Check for closed windows.
|
||||||
@@ -585,4 +721,22 @@ impl State {
|
|||||||
server.send_event(event);
|
server.send_event(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ipc_refresh_overview(&mut self) {
|
||||||
|
let Some(server) = &self.niri.ipc_server else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut state = server.event_stream_state.borrow_mut();
|
||||||
|
let state = &mut state.overview;
|
||||||
|
let is_open = self.niri.layout.is_overview_open();
|
||||||
|
|
||||||
|
if state.is_open == is_open {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let event = Event::OverviewOpenedOrClosed { is_open };
|
||||||
|
state.apply(event.clone());
|
||||||
|
server.send_event(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
use niri_config::layer_rule::LayerRule;
|
||||||
|
use niri_config::Config;
|
||||||
|
use smithay::backend::renderer::element::surface::{
|
||||||
|
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
|
||||||
|
};
|
||||||
|
use smithay::backend::renderer::element::Kind;
|
||||||
|
use smithay::desktop::{LayerSurface, PopupManager};
|
||||||
|
use smithay::utils::{Logical, Point, Scale, Size};
|
||||||
|
use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer};
|
||||||
|
|
||||||
|
use super::ResolvedLayerRules;
|
||||||
|
use crate::animation::Clock;
|
||||||
|
use crate::layout::shadow::Shadow;
|
||||||
|
use crate::niri_render_elements;
|
||||||
|
use crate::render_helpers::renderer::NiriRenderer;
|
||||||
|
use crate::render_helpers::shadow::ShadowRenderElement;
|
||||||
|
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||||
|
use crate::render_helpers::{RenderTarget, SplitElements};
|
||||||
|
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MappedLayer {
|
||||||
|
/// The surface itself.
|
||||||
|
surface: LayerSurface,
|
||||||
|
|
||||||
|
/// Up-to-date rules.
|
||||||
|
rules: ResolvedLayerRules,
|
||||||
|
|
||||||
|
/// Buffer to draw instead of the surface when it should be blocked out.
|
||||||
|
block_out_buffer: SolidColorBuffer,
|
||||||
|
|
||||||
|
/// The shadow around the surface.
|
||||||
|
shadow: Shadow,
|
||||||
|
|
||||||
|
/// The view size for the layer surface's output.
|
||||||
|
view_size: Size<f64, Logical>,
|
||||||
|
|
||||||
|
/// Scale of the output the layer surface is on (and rounds its sizes to).
|
||||||
|
scale: f64,
|
||||||
|
|
||||||
|
/// Clock for driving animations.
|
||||||
|
clock: Clock,
|
||||||
|
}
|
||||||
|
|
||||||
|
niri_render_elements! {
|
||||||
|
LayerSurfaceRenderElement<R> => {
|
||||||
|
Wayland = WaylandSurfaceRenderElement<R>,
|
||||||
|
SolidColor = SolidColorRenderElement,
|
||||||
|
Shadow = ShadowRenderElement,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MappedLayer {
|
||||||
|
pub fn new(
|
||||||
|
surface: LayerSurface,
|
||||||
|
rules: ResolvedLayerRules,
|
||||||
|
view_size: Size<f64, Logical>,
|
||||||
|
scale: f64,
|
||||||
|
clock: Clock,
|
||||||
|
config: &Config,
|
||||||
|
) -> Self {
|
||||||
|
let mut shadow_config = config.layout.shadow;
|
||||||
|
// Shadows for layer surfaces need to be explicitly enabled.
|
||||||
|
shadow_config.on = false;
|
||||||
|
let shadow_config = rules.shadow.resolve_against(shadow_config);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
surface,
|
||||||
|
rules,
|
||||||
|
block_out_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
|
||||||
|
view_size,
|
||||||
|
scale,
|
||||||
|
shadow: Shadow::new(shadow_config),
|
||||||
|
clock,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_config(&mut self, config: &Config) {
|
||||||
|
let mut shadow_config = config.layout.shadow;
|
||||||
|
// Shadows for layer surfaces need to be explicitly enabled.
|
||||||
|
shadow_config.on = false;
|
||||||
|
let shadow_config = self.rules.shadow.resolve_against(shadow_config);
|
||||||
|
self.shadow.update_config(shadow_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_shaders(&mut self) {
|
||||||
|
self.shadow.update_shaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_sizes(&mut self, view_size: Size<f64, Logical>, scale: f64) {
|
||||||
|
self.view_size = view_size;
|
||||||
|
self.scale = scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_render_elements(&mut self, size: Size<f64, Logical>) {
|
||||||
|
// Round to physical pixels.
|
||||||
|
let size = size
|
||||||
|
.to_physical_precise_round(self.scale)
|
||||||
|
.to_logical(self.scale);
|
||||||
|
|
||||||
|
self.block_out_buffer.resize(size);
|
||||||
|
|
||||||
|
let radius = self.rules.geometry_corner_radius.unwrap_or_default();
|
||||||
|
// FIXME: is_active based on keyboard focus?
|
||||||
|
self.shadow
|
||||||
|
.update_render_elements(size, true, radius, self.scale, 1.);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn are_animations_ongoing(&self) -> bool {
|
||||||
|
self.rules.baba_is_float
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn surface(&self) -> &LayerSurface {
|
||||||
|
&self.surface
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rules(&self) -> &ResolvedLayerRules {
|
||||||
|
&self.rules
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recomputes the resolved layer rules and returns whether they changed.
|
||||||
|
pub fn recompute_layer_rules(&mut self, rules: &[LayerRule], is_at_startup: bool) -> bool {
|
||||||
|
let new_rules = ResolvedLayerRules::compute(rules, &self.surface, is_at_startup);
|
||||||
|
if new_rules == self.rules {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rules = new_rules;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn place_within_backdrop(&self) -> bool {
|
||||||
|
if !self.rules.place_within_backdrop {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.surface.layer() != Layer::Background {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = self.surface.cached_state();
|
||||||
|
if state.exclusive_zone != ExclusiveZone::DontCare {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bob_offset(&self) -> Point<f64, Logical> {
|
||||||
|
if !self.rules.baba_is_float {
|
||||||
|
return Point::from((0., 0.));
|
||||||
|
}
|
||||||
|
|
||||||
|
let y = baba_is_float_offset(self.clock.now(), self.view_size.h);
|
||||||
|
let y = round_logical_in_physical(self.scale, y);
|
||||||
|
Point::from((0., y))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<R: NiriRenderer>(
|
||||||
|
&self,
|
||||||
|
renderer: &mut R,
|
||||||
|
location: Point<f64, Logical>,
|
||||||
|
target: RenderTarget,
|
||||||
|
) -> SplitElements<LayerSurfaceRenderElement<R>> {
|
||||||
|
let mut rv = SplitElements::default();
|
||||||
|
|
||||||
|
let scale = Scale::from(self.scale);
|
||||||
|
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
|
||||||
|
let location = location + self.bob_offset();
|
||||||
|
|
||||||
|
if target.should_block_out(self.rules.block_out_from) {
|
||||||
|
// Round to physical pixels.
|
||||||
|
let location = location.to_physical_precise_round(scale).to_logical(scale);
|
||||||
|
|
||||||
|
// FIXME: take geometry-corner-radius into account.
|
||||||
|
let elem = SolidColorRenderElement::from_buffer(
|
||||||
|
&self.block_out_buffer,
|
||||||
|
location,
|
||||||
|
alpha,
|
||||||
|
Kind::Unspecified,
|
||||||
|
);
|
||||||
|
rv.normal.push(elem.into());
|
||||||
|
} else {
|
||||||
|
// Layer surfaces don't have extra geometry like windows.
|
||||||
|
let buf_pos = location;
|
||||||
|
|
||||||
|
let surface = self.surface.wl_surface();
|
||||||
|
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
|
||||||
|
// Layer surfaces don't have extra geometry like windows.
|
||||||
|
let offset = popup_offset - popup.geometry().loc;
|
||||||
|
|
||||||
|
rv.popups.extend(render_elements_from_surface_tree(
|
||||||
|
renderer,
|
||||||
|
popup.wl_surface(),
|
||||||
|
(buf_pos + offset.to_f64()).to_physical_precise_round(scale),
|
||||||
|
scale,
|
||||||
|
alpha,
|
||||||
|
Kind::Unspecified,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.normal = render_elements_from_surface_tree(
|
||||||
|
renderer,
|
||||||
|
surface,
|
||||||
|
buf_pos.to_physical_precise_round(scale),
|
||||||
|
scale,
|
||||||
|
alpha,
|
||||||
|
Kind::Unspecified,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let location = location.to_physical_precise_round(scale).to_logical(scale);
|
||||||
|
rv.normal
|
||||||
|
.extend(self.shadow.render(renderer, location).map(Into::into));
|
||||||
|
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
use niri_config::layer_rule::{LayerRule, Match};
|
||||||
|
use niri_config::{BlockOutFrom, CornerRadius, ShadowRule};
|
||||||
|
use smithay::desktop::LayerSurface;
|
||||||
|
|
||||||
|
pub mod mapped;
|
||||||
|
pub use mapped::MappedLayer;
|
||||||
|
|
||||||
|
/// Rules fully resolved for a layer-shell surface.
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct ResolvedLayerRules {
|
||||||
|
/// Extra opacity to draw this layer surface with.
|
||||||
|
pub opacity: Option<f32>,
|
||||||
|
|
||||||
|
/// Whether to block out this layer surface from certain render targets.
|
||||||
|
pub block_out_from: Option<BlockOutFrom>,
|
||||||
|
|
||||||
|
/// Shadow overrides.
|
||||||
|
pub shadow: ShadowRule,
|
||||||
|
|
||||||
|
/// Corner radius to assume this layer surface has.
|
||||||
|
pub geometry_corner_radius: Option<CornerRadius>,
|
||||||
|
|
||||||
|
/// Whether to place this layer surface within the overview backdrop.
|
||||||
|
pub place_within_backdrop: bool,
|
||||||
|
|
||||||
|
/// Whether to bob this window up and down.
|
||||||
|
pub baba_is_float: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolvedLayerRules {
|
||||||
|
pub const fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
opacity: None,
|
||||||
|
block_out_from: None,
|
||||||
|
shadow: ShadowRule {
|
||||||
|
off: false,
|
||||||
|
on: false,
|
||||||
|
offset: None,
|
||||||
|
softness: None,
|
||||||
|
spread: None,
|
||||||
|
draw_behind_window: None,
|
||||||
|
color: None,
|
||||||
|
inactive_color: None,
|
||||||
|
},
|
||||||
|
geometry_corner_radius: None,
|
||||||
|
place_within_backdrop: false,
|
||||||
|
baba_is_float: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute(rules: &[LayerRule], surface: &LayerSurface, is_at_startup: bool) -> Self {
|
||||||
|
let _span = tracy_client::span!("ResolvedLayerRules::compute");
|
||||||
|
|
||||||
|
let mut resolved = ResolvedLayerRules::empty();
|
||||||
|
|
||||||
|
for rule in rules {
|
||||||
|
let matches = |m: &Match| {
|
||||||
|
if let Some(at_startup) = m.at_startup {
|
||||||
|
if at_startup != is_at_startup {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
surface_matches(surface, m)
|
||||||
|
};
|
||||||
|
|
||||||
|
if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.excludes.iter().any(matches) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = rule.opacity {
|
||||||
|
resolved.opacity = Some(x);
|
||||||
|
}
|
||||||
|
if let Some(x) = rule.block_out_from {
|
||||||
|
resolved.block_out_from = Some(x);
|
||||||
|
}
|
||||||
|
if let Some(x) = rule.geometry_corner_radius {
|
||||||
|
resolved.geometry_corner_radius = Some(x);
|
||||||
|
}
|
||||||
|
if let Some(x) = rule.place_within_backdrop {
|
||||||
|
resolved.place_within_backdrop = x;
|
||||||
|
}
|
||||||
|
if let Some(x) = rule.baba_is_float {
|
||||||
|
resolved.baba_is_float = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved.shadow.merge_with(&rule.shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn surface_matches(surface: &LayerSurface, m: &Match) -> bool {
|
||||||
|
if let Some(namespace_re) = &m.namespace {
|
||||||
|
if !namespace_re.0.is_match(surface.namespace()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use glam::{Mat3, Vec2};
|
use glam::{Mat3, Vec2};
|
||||||
@@ -138,16 +137,15 @@ impl ClosingWindow {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn advance_animations(&mut self, current_time: Duration) {
|
pub fn advance_animations(&mut self) {
|
||||||
match &mut self.anim_state {
|
match &mut self.anim_state {
|
||||||
AnimationState::Waiting { blocker, anim } => {
|
AnimationState::Waiting { blocker, anim } => {
|
||||||
if blocker.state() != BlockerState::Pending {
|
if blocker.state() != BlockerState::Pending {
|
||||||
let mut anim = anim.restarted(0., 1., 0.);
|
let anim = anim.restarted(0., 1., 0.);
|
||||||
anim.set_current_time(current_time);
|
|
||||||
self.anim_state = AnimationState::Animating(anim);
|
self.anim_state = AnimationState::Animating(anim);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AnimationState::Animating(anim) => anim.set_current_time(current_time),
|
AnimationState::Animating(_anim) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+26
-20
@@ -1,8 +1,8 @@
|
|||||||
use std::iter::zip;
|
use std::iter::zip;
|
||||||
|
|
||||||
use arrayvec::ArrayVec;
|
use arrayvec::ArrayVec;
|
||||||
use niri_config::{CornerRadius, Gradient, GradientInterpolation, GradientRelativeTo};
|
use niri_config::{CornerRadius, Gradient, GradientRelativeTo};
|
||||||
use smithay::backend::renderer::element::Kind;
|
use smithay::backend::renderer::element::{Element as _, Kind};
|
||||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||||
|
|
||||||
use crate::niri_render_elements;
|
use crate::niri_render_elements;
|
||||||
@@ -53,19 +53,24 @@ impl FocusRing {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn update_render_elements(
|
pub fn update_render_elements(
|
||||||
&mut self,
|
&mut self,
|
||||||
win_size: Size<f64, Logical>,
|
win_size: Size<f64, Logical>,
|
||||||
is_active: bool,
|
is_active: bool,
|
||||||
is_border: bool,
|
is_border: bool,
|
||||||
|
is_urgent: bool,
|
||||||
view_rect: Rectangle<f64, Logical>,
|
view_rect: Rectangle<f64, Logical>,
|
||||||
radius: CornerRadius,
|
radius: CornerRadius,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
|
alpha: f32,
|
||||||
) {
|
) {
|
||||||
let width = self.config.width.0;
|
let width = self.config.width.0;
|
||||||
self.full_size = win_size + Size::from((width, width)).upscale(2.);
|
self.full_size = win_size + Size::from((width, width)).upscale(2.);
|
||||||
|
|
||||||
let color = if is_active {
|
let color = if is_urgent {
|
||||||
|
self.config.urgent_color
|
||||||
|
} else if is_active {
|
||||||
self.config.active_color
|
self.config.active_color
|
||||||
} else {
|
} else {
|
||||||
self.config.inactive_color
|
self.config.inactive_color
|
||||||
@@ -77,7 +82,9 @@ impl FocusRing {
|
|||||||
|
|
||||||
let radius = radius.fit_to(self.full_size.w as f32, self.full_size.h as f32);
|
let radius = radius.fit_to(self.full_size.w as f32, self.full_size.h as f32);
|
||||||
|
|
||||||
let gradient = if is_active {
|
let gradient = if is_urgent {
|
||||||
|
self.config.urgent_gradient
|
||||||
|
} else if is_active {
|
||||||
self.config.active_gradient
|
self.config.active_gradient
|
||||||
} else {
|
} else {
|
||||||
self.config.inactive_gradient
|
self.config.inactive_gradient
|
||||||
@@ -86,15 +93,9 @@ impl FocusRing {
|
|||||||
self.use_border_shader = radius != CornerRadius::default() || gradient.is_some();
|
self.use_border_shader = radius != CornerRadius::default() || gradient.is_some();
|
||||||
|
|
||||||
// Set the defaults for solid color + rounded corners.
|
// Set the defaults for solid color + rounded corners.
|
||||||
let gradient = gradient.unwrap_or(Gradient {
|
let gradient = gradient.unwrap_or_else(|| Gradient::from(color));
|
||||||
from: color,
|
|
||||||
to: color,
|
|
||||||
angle: 0,
|
|
||||||
relative_to: GradientRelativeTo::Window,
|
|
||||||
in_: GradientInterpolation::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let full_rect = Rectangle::from_loc_and_size((-width, -width), self.full_size);
|
let full_rect = Rectangle::new(Point::from((-width, -width)), self.full_size);
|
||||||
let gradient_area = match gradient.relative_to {
|
let gradient_area = match gradient.relative_to {
|
||||||
GradientRelativeTo::Window => full_rect,
|
GradientRelativeTo::Window => full_rect,
|
||||||
GradientRelativeTo::WorkspaceView => view_rect,
|
GradientRelativeTo::WorkspaceView => view_rect,
|
||||||
@@ -178,15 +179,16 @@ impl FocusRing {
|
|||||||
for (border, (loc, size)) in zip(&mut self.borders, zip(self.locations, self.sizes)) {
|
for (border, (loc, size)) in zip(&mut self.borders, zip(self.locations, self.sizes)) {
|
||||||
border.update(
|
border.update(
|
||||||
size,
|
size,
|
||||||
Rectangle::from_loc_and_size(gradient_area.loc - loc, gradient_area.size),
|
Rectangle::new(gradient_area.loc - loc, gradient_area.size),
|
||||||
gradient.in_,
|
gradient.in_,
|
||||||
gradient.from,
|
gradient.from,
|
||||||
gradient.to,
|
gradient.to,
|
||||||
((gradient.angle as f32) - 90.).to_radians(),
|
((gradient.angle as f32) - 90.).to_radians(),
|
||||||
Rectangle::from_loc_and_size(full_rect.loc - loc, full_rect.size),
|
Rectangle::new(full_rect.loc - loc, full_rect.size),
|
||||||
rounded_corner_border_width,
|
rounded_corner_border_width,
|
||||||
radius,
|
radius,
|
||||||
scale as f32,
|
scale as f32,
|
||||||
|
alpha,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -196,18 +198,16 @@ impl FocusRing {
|
|||||||
|
|
||||||
self.borders[0].update(
|
self.borders[0].update(
|
||||||
self.sizes[0],
|
self.sizes[0],
|
||||||
Rectangle::from_loc_and_size(
|
Rectangle::new(gradient_area.loc - self.locations[0], gradient_area.size),
|
||||||
gradient_area.loc - self.locations[0],
|
|
||||||
gradient_area.size,
|
|
||||||
),
|
|
||||||
gradient.in_,
|
gradient.in_,
|
||||||
gradient.from,
|
gradient.from,
|
||||||
gradient.to,
|
gradient.to,
|
||||||
((gradient.angle as f32) - 90.).to_radians(),
|
((gradient.angle as f32) - 90.).to_radians(),
|
||||||
Rectangle::from_loc_and_size(full_rect.loc - self.locations[0], full_rect.size),
|
Rectangle::new(full_rect.loc - self.locations[0], full_rect.size),
|
||||||
rounded_corner_border_width,
|
rounded_corner_border_width,
|
||||||
radius,
|
radius,
|
||||||
scale as f32,
|
scale as f32,
|
||||||
|
alpha,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +238,9 @@ impl FocusRing {
|
|||||||
let elem = if self.use_border_shader && has_border_shader {
|
let elem = if self.use_border_shader && has_border_shader {
|
||||||
border.clone().with_location(location).into()
|
border.clone().with_location(location).into()
|
||||||
} else {
|
} else {
|
||||||
SolidColorRenderElement::from_buffer(buffer, location, 1., Kind::Unspecified).into()
|
let alpha = border.alpha();
|
||||||
|
SolidColorRenderElement::from_buffer(buffer, location, alpha, Kind::Unspecified)
|
||||||
|
.into()
|
||||||
};
|
};
|
||||||
rv.push(elem);
|
rv.push(elem);
|
||||||
};
|
};
|
||||||
@@ -265,4 +267,8 @@ impl FocusRing {
|
|||||||
pub fn is_off(&self) -> bool {
|
pub fn is_off(&self) -> bool {
|
||||||
self.config.off
|
self.config.off
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn config(&self) -> &niri_config::FocusRing {
|
||||||
|
&self.config
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ impl InsertHintElement {
|
|||||||
width: FloatOrInt(0.),
|
width: FloatOrInt(0.),
|
||||||
active_color: config.color,
|
active_color: config.color,
|
||||||
inactive_color: config.color,
|
inactive_color: config.color,
|
||||||
|
urgent_color: config.color,
|
||||||
active_gradient: config.gradient,
|
active_gradient: config.gradient,
|
||||||
inactive_gradient: config.gradient,
|
inactive_gradient: config.gradient,
|
||||||
|
urgent_gradient: config.gradient,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,8 +33,10 @@ impl InsertHintElement {
|
|||||||
width: FloatOrInt(0.),
|
width: FloatOrInt(0.),
|
||||||
active_color: config.color,
|
active_color: config.color,
|
||||||
inactive_color: config.color,
|
inactive_color: config.color,
|
||||||
|
urgent_color: config.color,
|
||||||
active_gradient: config.gradient,
|
active_gradient: config.gradient,
|
||||||
inactive_gradient: config.gradient,
|
inactive_gradient: config.gradient,
|
||||||
|
urgent_gradient: config.gradient,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +52,7 @@ impl InsertHintElement {
|
|||||||
scale: f64,
|
scale: f64,
|
||||||
) {
|
) {
|
||||||
self.inner
|
self.inner
|
||||||
.update_render_elements(size, true, false, view_rect, radius, scale);
|
.update_render_elements(size, true, false, false, view_rect, radius, scale, 1.);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(
|
pub fn render(
|
||||||
|
|||||||
+2464
-3085
File diff suppressed because it is too large
Load Diff
+1276
-521
File diff suppressed because it is too large
Load Diff
@@ -1,34 +1,31 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use glam::{Mat3, Vec2};
|
use glam::{Mat3, Vec2};
|
||||||
use smithay::backend::allocator::Fourcc;
|
|
||||||
use smithay::backend::renderer::element::utils::{
|
use smithay::backend::renderer::element::utils::{
|
||||||
Relocate, RelocateRenderElement, RescaleRenderElement,
|
Relocate, RelocateRenderElement, RescaleRenderElement,
|
||||||
};
|
};
|
||||||
use smithay::backend::renderer::element::{Kind, RenderElement};
|
use smithay::backend::renderer::element::{Element as _, Kind, RenderElement};
|
||||||
use smithay::backend::renderer::gles::{GlesRenderer, Uniform};
|
use smithay::backend::renderer::gles::{GlesRenderer, Uniform};
|
||||||
use smithay::backend::renderer::Texture;
|
use smithay::backend::renderer::Texture;
|
||||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
|
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
||||||
|
|
||||||
use crate::animation::Animation;
|
use crate::animation::Animation;
|
||||||
use crate::niri_render_elements;
|
use crate::niri_render_elements;
|
||||||
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
use crate::render_helpers::offscreen::{OffscreenBuffer, OffscreenData, OffscreenRenderElement};
|
||||||
use crate::render_helpers::render_to_encompassing_texture;
|
|
||||||
use crate::render_helpers::shader_element::ShaderRenderElement;
|
use crate::render_helpers::shader_element::ShaderRenderElement;
|
||||||
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
|
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
|
||||||
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct OpenAnimation {
|
pub struct OpenAnimation {
|
||||||
anim: Animation,
|
anim: Animation,
|
||||||
random_seed: f32,
|
random_seed: f32,
|
||||||
|
buffer: OffscreenBuffer,
|
||||||
}
|
}
|
||||||
|
|
||||||
niri_render_elements! {
|
niri_render_elements! {
|
||||||
OpeningWindowRenderElement => {
|
OpeningWindowRenderElement => {
|
||||||
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
|
Offscreen = RelocateRenderElement<RescaleRenderElement<OffscreenRenderElement>>,
|
||||||
Shader = ShaderRenderElement,
|
Shader = ShaderRenderElement,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,12 +35,11 @@ impl OpenAnimation {
|
|||||||
Self {
|
Self {
|
||||||
anim,
|
anim,
|
||||||
random_seed: fastrand::f32(),
|
random_seed: fastrand::f32(),
|
||||||
|
buffer: OffscreenBuffer::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn advance_animations(&mut self, current_time: Duration) {
|
pub fn advance_animations(&mut self) {}
|
||||||
self.anim.set_current_time(current_time);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_done(&self) -> bool {
|
pub fn is_done(&self) -> bool {
|
||||||
self.anim.is_done()
|
self.anim.is_done()
|
||||||
@@ -58,24 +54,24 @@ impl OpenAnimation {
|
|||||||
geo_size: Size<f64, Logical>,
|
geo_size: Size<f64, Logical>,
|
||||||
location: Point<f64, Logical>,
|
location: Point<f64, Logical>,
|
||||||
scale: Scale<f64>,
|
scale: Scale<f64>,
|
||||||
) -> anyhow::Result<OpeningWindowRenderElement> {
|
alpha: f32,
|
||||||
|
) -> anyhow::Result<(OpeningWindowRenderElement, OffscreenData)> {
|
||||||
let progress = self.anim.value();
|
let progress = self.anim.value();
|
||||||
let clamped_progress = self.anim.clamped_value().clamp(0., 1.);
|
let clamped_progress = self.anim.clamped_value().clamp(0., 1.);
|
||||||
|
|
||||||
let (texture, _sync_point, geo) = render_to_encompassing_texture(
|
let (elem, _sync_point, mut data) = self
|
||||||
renderer,
|
.buffer
|
||||||
scale,
|
.render(renderer, scale, elements)
|
||||||
Transform::Normal,
|
.context("error rendering to offscreen buffer")?;
|
||||||
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() {
|
if Shaders::get(renderer).program(ProgramType::Open).is_some() {
|
||||||
let mut area = Rectangle::from_loc_and_size(location + offset, texture_size);
|
// OffscreenBuffer renders with Transform::Normal and the scale that we passed, so we
|
||||||
|
// can assume that below.
|
||||||
|
let offset = elem.offset();
|
||||||
|
let texture = elem.texture();
|
||||||
|
let texture_size = elem.logical_size();
|
||||||
|
|
||||||
|
let mut area = Rectangle::new(location + offset, texture_size);
|
||||||
|
|
||||||
// Expand the area a bit to allow for more varied effects.
|
// Expand the area a bit to allow for more varied effects.
|
||||||
let mut target_size = area.size.upscale(1.5);
|
let mut target_size = area.size.upscale(1.5);
|
||||||
@@ -102,12 +98,12 @@ impl OpenAnimation {
|
|||||||
let geo_to_tex =
|
let geo_to_tex =
|
||||||
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
|
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
|
||||||
|
|
||||||
return Ok(ShaderRenderElement::new(
|
let elem = ShaderRenderElement::new(
|
||||||
ProgramType::Open,
|
ProgramType::Open,
|
||||||
area.size,
|
area.size,
|
||||||
None,
|
None,
|
||||||
scale.x as f32,
|
scale.x as f32,
|
||||||
1.,
|
alpha,
|
||||||
vec![
|
vec![
|
||||||
mat3_uniform("niri_input_to_geo", input_to_geo),
|
mat3_uniform("niri_input_to_geo", input_to_geo),
|
||||||
Uniform::new("niri_geo_size", geo_size.to_array()),
|
Uniform::new("niri_geo_size", geo_size.to_array()),
|
||||||
@@ -119,36 +115,29 @@ impl OpenAnimation {
|
|||||||
HashMap::from([(String::from("niri_tex"), texture.clone())]),
|
HashMap::from([(String::from("niri_tex"), texture.clone())]),
|
||||||
Kind::Unspecified,
|
Kind::Unspecified,
|
||||||
)
|
)
|
||||||
.with_location(area.loc)
|
.with_location(area.loc);
|
||||||
.into());
|
|
||||||
|
// We're drawing the shader, not the offscreen itself.
|
||||||
|
data.id = elem.id().clone();
|
||||||
|
|
||||||
|
return Ok((elem.into(), data));
|
||||||
}
|
}
|
||||||
|
|
||||||
let buffer =
|
let elem = elem.with_alpha(clamped_progress as f32 * alpha);
|
||||||
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 center = geo_size.to_point().downscale(2.);
|
||||||
let elem = RescaleRenderElement::from_element(
|
let elem = RescaleRenderElement::from_element(
|
||||||
elem,
|
elem,
|
||||||
(center - offset).to_physical_precise_round(scale),
|
center.to_physical_precise_round(scale),
|
||||||
(progress / 2. + 0.5).max(0.),
|
(progress / 2. + 0.5).max(0.),
|
||||||
);
|
);
|
||||||
|
|
||||||
let elem = RelocateRenderElement::from_element(
|
let elem = RelocateRenderElement::from_element(
|
||||||
elem,
|
elem,
|
||||||
(location + offset).to_physical_precise_round(scale),
|
location.to_physical_precise_round(scale),
|
||||||
Relocate::Relative,
|
Relocate::Relative,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(elem.into())
|
Ok((elem.into(), data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,184 @@
|
|||||||
|
use std::iter::zip;
|
||||||
|
|
||||||
|
use niri_config::CornerRadius;
|
||||||
|
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||||
|
|
||||||
|
use crate::render_helpers::renderer::NiriRenderer;
|
||||||
|
use crate::render_helpers::shadow::ShadowRenderElement;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Shadow {
|
||||||
|
shader_rects: Vec<Rectangle<f64, Logical>>,
|
||||||
|
shaders: Vec<ShadowRenderElement>,
|
||||||
|
config: niri_config::Shadow,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shadow {
|
||||||
|
pub fn new(config: niri_config::Shadow) -> Self {
|
||||||
|
Self {
|
||||||
|
shader_rects: Vec::new(),
|
||||||
|
shaders: Vec::new(),
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_config(&mut self, config: niri_config::Shadow) {
|
||||||
|
self.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_shaders(&mut self) {
|
||||||
|
for elem in &mut self.shaders {
|
||||||
|
elem.damage_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_render_elements(
|
||||||
|
&mut self,
|
||||||
|
win_size: Size<f64, Logical>,
|
||||||
|
is_active: bool,
|
||||||
|
radius: CornerRadius,
|
||||||
|
scale: f64,
|
||||||
|
alpha: f32,
|
||||||
|
) {
|
||||||
|
let ceil = |logical: f64| (logical * scale).ceil() / scale;
|
||||||
|
|
||||||
|
// All of this stuff should end up aligned to physical pixels because:
|
||||||
|
// * Window size is rounded to physical pixels before being passed to this function.
|
||||||
|
// * We will ceil the corner radii below.
|
||||||
|
// * We do not divide anything, only add, subtract and multiply by integers.
|
||||||
|
// * At rendering time, tile positions are rounded to physical pixels.
|
||||||
|
|
||||||
|
let width = self.config.softness.0;
|
||||||
|
// Like in CSS box-shadow.
|
||||||
|
let sigma = width / 2.;
|
||||||
|
// Adjust width to draw all necessary pixels.
|
||||||
|
let width = ceil(sigma * 3.);
|
||||||
|
|
||||||
|
let offset = self.config.offset;
|
||||||
|
let offset = Point::from((ceil(offset.x.0), ceil(offset.y.0)));
|
||||||
|
|
||||||
|
let spread = self.config.spread.0;
|
||||||
|
let spread = ceil(spread.abs()).copysign(spread);
|
||||||
|
let offset = offset - Point::from((spread, spread));
|
||||||
|
|
||||||
|
let win_radius = radius.fit_to(win_size.w as f32, win_size.h as f32);
|
||||||
|
|
||||||
|
let box_size = if spread >= 0. {
|
||||||
|
win_size + Size::from((spread, spread)).upscale(2.)
|
||||||
|
} else {
|
||||||
|
// This is a saturating sub.
|
||||||
|
win_size - Size::from((-spread, -spread)).upscale(2.)
|
||||||
|
};
|
||||||
|
let radius = win_radius.expanded_by(spread as f32);
|
||||||
|
|
||||||
|
let shader_size = box_size + Size::from((width, width)).upscale(2.);
|
||||||
|
|
||||||
|
let color = if is_active {
|
||||||
|
self.config.color
|
||||||
|
} else {
|
||||||
|
// Default to slightly more transparent.
|
||||||
|
self.config
|
||||||
|
.inactive_color
|
||||||
|
.unwrap_or(self.config.color * 0.75)
|
||||||
|
};
|
||||||
|
|
||||||
|
let shader_geo = Rectangle::new(Point::from((-width, -width)), shader_size);
|
||||||
|
|
||||||
|
// This is actually offset relative to shader_geo, this is handled below.
|
||||||
|
let window_geo = Rectangle::new(Point::from((0., 0.)), win_size);
|
||||||
|
|
||||||
|
if !self.config.draw_behind_window {
|
||||||
|
let top_left = ceil(f64::from(win_radius.top_left));
|
||||||
|
let top_right = f64::min(win_size.w - top_left, ceil(f64::from(win_radius.top_right)));
|
||||||
|
let bottom_left = f64::min(
|
||||||
|
win_size.h - top_left,
|
||||||
|
ceil(f64::from(win_radius.bottom_left)),
|
||||||
|
);
|
||||||
|
let bottom_right = f64::min(
|
||||||
|
win_size.h - top_right,
|
||||||
|
f64::min(
|
||||||
|
win_size.w - bottom_left,
|
||||||
|
ceil(f64::from(win_radius.bottom_right)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let top_left = Rectangle::new(Point::from((0., 0.)), Size::from((top_left, top_left)));
|
||||||
|
let top_right = Rectangle::new(
|
||||||
|
Point::from((win_size.w - top_right, 0.)),
|
||||||
|
Size::from((top_right, top_right)),
|
||||||
|
);
|
||||||
|
let bottom_right = Rectangle::new(
|
||||||
|
Point::from((win_size.w - bottom_right, win_size.h - bottom_right)),
|
||||||
|
Size::from((bottom_right, bottom_right)),
|
||||||
|
);
|
||||||
|
let bottom_left = Rectangle::new(
|
||||||
|
Point::from((0., win_size.h - bottom_left)),
|
||||||
|
Size::from((bottom_left, bottom_left)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut background =
|
||||||
|
window_geo.subtract_rects([top_left, top_right, bottom_right, bottom_left]);
|
||||||
|
for rect in &mut background {
|
||||||
|
rect.loc -= offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.shader_rects = shader_geo.subtract_rects(background);
|
||||||
|
self.shaders
|
||||||
|
.resize_with(self.shader_rects.len(), Default::default);
|
||||||
|
|
||||||
|
for (shader, rect) in zip(&mut self.shaders, &mut self.shader_rects) {
|
||||||
|
shader.update(
|
||||||
|
rect.size,
|
||||||
|
Rectangle::new(rect.loc.upscale(-1.), box_size),
|
||||||
|
color,
|
||||||
|
sigma as f32,
|
||||||
|
radius,
|
||||||
|
scale as f32,
|
||||||
|
Rectangle::new(window_geo.loc - offset - rect.loc, window_geo.size),
|
||||||
|
win_radius,
|
||||||
|
alpha,
|
||||||
|
);
|
||||||
|
|
||||||
|
rect.loc += offset;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.shader_rects.resize_with(1, Default::default);
|
||||||
|
self.shader_rects[0] = shader_geo;
|
||||||
|
|
||||||
|
self.shaders.resize_with(1, Default::default);
|
||||||
|
self.shaders[0].update(
|
||||||
|
shader_geo.size,
|
||||||
|
Rectangle::new(shader_geo.loc.upscale(-1.), box_size),
|
||||||
|
color,
|
||||||
|
sigma as f32,
|
||||||
|
radius,
|
||||||
|
scale as f32,
|
||||||
|
Rectangle::zero(),
|
||||||
|
Default::default(),
|
||||||
|
alpha,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.shader_rects[0].loc += offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(
|
||||||
|
&self,
|
||||||
|
renderer: &mut impl NiriRenderer,
|
||||||
|
location: Point<f64, Logical>,
|
||||||
|
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
|
||||||
|
if !self.config.on {
|
||||||
|
return None.into_iter().flatten();
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_shadow_shader = ShadowRenderElement::has_shader(renderer);
|
||||||
|
if !has_shadow_shader {
|
||||||
|
return None.into_iter().flatten();
|
||||||
|
}
|
||||||
|
|
||||||
|
let rv = zip(&self.shaders, &self.shader_rects)
|
||||||
|
.map(move |(shader, rect)| shader.clone().with_location(location + rect.loc));
|
||||||
|
|
||||||
|
Some(rv).into_iter().flatten()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
use std::iter::zip;
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
use niri_config::{CornerRadius, Gradient, GradientRelativeTo, TabIndicatorPosition};
|
||||||
|
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||||
|
|
||||||
|
use super::tile::Tile;
|
||||||
|
use super::LayoutElement;
|
||||||
|
use crate::animation::{Animation, Clock};
|
||||||
|
use crate::niri_render_elements;
|
||||||
|
use crate::render_helpers::border::BorderRenderElement;
|
||||||
|
use crate::render_helpers::renderer::NiriRenderer;
|
||||||
|
use crate::utils::{
|
||||||
|
floor_logical_in_physical_max1, round_logical_in_physical, round_logical_in_physical_max1,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TabIndicator {
|
||||||
|
shader_locs: Vec<Point<f64, Logical>>,
|
||||||
|
shaders: Vec<BorderRenderElement>,
|
||||||
|
open_anim: Option<Animation>,
|
||||||
|
config: niri_config::TabIndicator,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TabInfo {
|
||||||
|
/// Gradient for the tab indicator.
|
||||||
|
pub gradient: Gradient,
|
||||||
|
/// Tab geometry in the same coordinate system as the area.
|
||||||
|
pub geometry: Rectangle<f64, Logical>,
|
||||||
|
}
|
||||||
|
|
||||||
|
niri_render_elements! {
|
||||||
|
TabIndicatorRenderElement => {
|
||||||
|
Gradient = BorderRenderElement,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TabIndicator {
|
||||||
|
pub fn new(config: niri_config::TabIndicator) -> Self {
|
||||||
|
Self {
|
||||||
|
shader_locs: Vec::new(),
|
||||||
|
shaders: Vec::new(),
|
||||||
|
open_anim: None,
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_config(&mut self, config: niri_config::TabIndicator) {
|
||||||
|
self.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_shaders(&mut self) {
|
||||||
|
for elem in &mut self.shaders {
|
||||||
|
elem.damage_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advance_animations(&mut self) {
|
||||||
|
if let Some(anim) = &mut self.open_anim {
|
||||||
|
if anim.is_done() {
|
||||||
|
self.open_anim = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn are_animations_ongoing(&self) -> bool {
|
||||||
|
self.open_anim.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_open_animation(&mut self, clock: Clock, config: niri_config::Animation) {
|
||||||
|
self.open_anim = Some(Animation::new(clock, 0., 1., 0., config));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_rects(
|
||||||
|
&self,
|
||||||
|
area: Rectangle<f64, Logical>,
|
||||||
|
count: usize,
|
||||||
|
scale: f64,
|
||||||
|
) -> impl Iterator<Item = Rectangle<f64, Logical>> {
|
||||||
|
let round = |logical: f64| round_logical_in_physical(scale, logical);
|
||||||
|
let round_max1 = |logical: f64| round_logical_in_physical_max1(scale, logical);
|
||||||
|
|
||||||
|
let progress = self.open_anim.as_ref().map_or(1., |a| a.value().max(0.));
|
||||||
|
|
||||||
|
let width = round_max1(self.config.width.0);
|
||||||
|
let gap = self.config.gap.0;
|
||||||
|
let gap = round_max1(gap.abs()).copysign(gap);
|
||||||
|
let gaps_between = round_max1(self.config.gaps_between_tabs.0);
|
||||||
|
|
||||||
|
let position = self.config.position;
|
||||||
|
let side = match position {
|
||||||
|
TabIndicatorPosition::Left | TabIndicatorPosition::Right => area.size.h,
|
||||||
|
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => area.size.w,
|
||||||
|
};
|
||||||
|
let total_prop = self.config.length.total_proportion.unwrap_or(0.5);
|
||||||
|
let min_length = round(side * total_prop.clamp(0., 2.));
|
||||||
|
|
||||||
|
// Compute px_per_tab before applying the animation to gaps_between in order to avoid it
|
||||||
|
// growing and shrinking over the duration of the animation.
|
||||||
|
let pixel = 1. / scale;
|
||||||
|
let shortest_length = count as f64 * (pixel + gaps_between) - gaps_between;
|
||||||
|
let length = f64::max(min_length, shortest_length);
|
||||||
|
let px_per_tab = (length + gaps_between) / count as f64 - gaps_between;
|
||||||
|
|
||||||
|
let px_per_tab = px_per_tab * progress;
|
||||||
|
let gaps_between = round(self.config.gaps_between_tabs.0 * progress);
|
||||||
|
|
||||||
|
let length = count as f64 * (px_per_tab + gaps_between) - gaps_between;
|
||||||
|
let px_per_tab = floor_logical_in_physical_max1(scale, px_per_tab);
|
||||||
|
let floored_length = count as f64 * (px_per_tab + gaps_between) - gaps_between;
|
||||||
|
let mut ones_left = ((length - floored_length) / pixel).round() as usize;
|
||||||
|
|
||||||
|
let mut shader_loc = Point::from((-gap - width, round((side - length) / 2.)));
|
||||||
|
match position {
|
||||||
|
TabIndicatorPosition::Left => (),
|
||||||
|
TabIndicatorPosition::Right => shader_loc.x = area.size.w + gap,
|
||||||
|
TabIndicatorPosition::Top => mem::swap(&mut shader_loc.x, &mut shader_loc.y),
|
||||||
|
TabIndicatorPosition::Bottom => {
|
||||||
|
shader_loc.x = shader_loc.y;
|
||||||
|
shader_loc.y = area.size.h + gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shader_loc += area.loc;
|
||||||
|
|
||||||
|
(0..count).map(move |_| {
|
||||||
|
let mut px_per_tab = px_per_tab;
|
||||||
|
if ones_left > 0 {
|
||||||
|
ones_left -= 1;
|
||||||
|
px_per_tab += pixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
let loc = shader_loc;
|
||||||
|
|
||||||
|
match position {
|
||||||
|
TabIndicatorPosition::Left | TabIndicatorPosition::Right => {
|
||||||
|
shader_loc.y += px_per_tab + gaps_between
|
||||||
|
}
|
||||||
|
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => {
|
||||||
|
shader_loc.x += px_per_tab + gaps_between
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = match position {
|
||||||
|
TabIndicatorPosition::Left | TabIndicatorPosition::Right => {
|
||||||
|
Size::from((width, px_per_tab))
|
||||||
|
}
|
||||||
|
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => {
|
||||||
|
Size::from((px_per_tab, width))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Rectangle::new(loc, size)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn update_render_elements(
|
||||||
|
&mut self,
|
||||||
|
enabled: bool,
|
||||||
|
// Geometry of the tabs area.
|
||||||
|
area: Rectangle<f64, Logical>,
|
||||||
|
// View rect relative to the tabs area.
|
||||||
|
area_view_rect: Rectangle<f64, Logical>,
|
||||||
|
// Tab count, should match the tabs iterator length.
|
||||||
|
tab_count: usize,
|
||||||
|
tabs: impl Iterator<Item = TabInfo>,
|
||||||
|
is_active: bool,
|
||||||
|
scale: f64,
|
||||||
|
) {
|
||||||
|
if !enabled || self.config.off {
|
||||||
|
self.shader_locs.clear();
|
||||||
|
self.shaders.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = tab_count;
|
||||||
|
if self.config.hide_when_single_tab && count == 1 {
|
||||||
|
self.shader_locs.clear();
|
||||||
|
self.shaders.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.shaders.resize_with(count, Default::default);
|
||||||
|
self.shader_locs.resize_with(count, Default::default);
|
||||||
|
|
||||||
|
let position = self.config.position;
|
||||||
|
let radius = self.config.corner_radius.0 as f32;
|
||||||
|
let shared_rounded_corners = self.config.gaps_between_tabs.0 == 0.;
|
||||||
|
let mut tabs_left = tab_count;
|
||||||
|
|
||||||
|
let rects = self.tab_rects(area, count, scale);
|
||||||
|
for ((shader, loc), (tab, rect)) in zip(
|
||||||
|
zip(&mut self.shaders, &mut self.shader_locs),
|
||||||
|
zip(tabs, rects),
|
||||||
|
) {
|
||||||
|
*loc = rect.loc;
|
||||||
|
|
||||||
|
let mut gradient_area = match tab.gradient.relative_to {
|
||||||
|
GradientRelativeTo::Window => tab.geometry,
|
||||||
|
GradientRelativeTo::WorkspaceView => area_view_rect,
|
||||||
|
};
|
||||||
|
gradient_area.loc -= *loc;
|
||||||
|
|
||||||
|
let mut color_from = tab.gradient.from;
|
||||||
|
let mut color_to = tab.gradient.to;
|
||||||
|
if !is_active {
|
||||||
|
color_from *= 0.5;
|
||||||
|
color_to *= 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
let radius = if shared_rounded_corners && tab_count > 1 {
|
||||||
|
if tabs_left == tab_count {
|
||||||
|
// First tab.
|
||||||
|
match position {
|
||||||
|
TabIndicatorPosition::Left | TabIndicatorPosition::Right => CornerRadius {
|
||||||
|
top_left: radius,
|
||||||
|
top_right: radius,
|
||||||
|
bottom_right: 0.,
|
||||||
|
bottom_left: 0.,
|
||||||
|
},
|
||||||
|
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => CornerRadius {
|
||||||
|
top_left: radius,
|
||||||
|
top_right: 0.,
|
||||||
|
bottom_right: 0.,
|
||||||
|
bottom_left: radius,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if tabs_left == 1 {
|
||||||
|
// Last tab.
|
||||||
|
match position {
|
||||||
|
TabIndicatorPosition::Left | TabIndicatorPosition::Right => CornerRadius {
|
||||||
|
top_left: 0.,
|
||||||
|
top_right: 0.,
|
||||||
|
bottom_right: radius,
|
||||||
|
bottom_left: radius,
|
||||||
|
},
|
||||||
|
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => CornerRadius {
|
||||||
|
top_left: 0.,
|
||||||
|
top_right: radius,
|
||||||
|
bottom_right: radius,
|
||||||
|
bottom_left: 0.,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Tab in the middle.
|
||||||
|
CornerRadius::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Separate tabs, or the only tab.
|
||||||
|
CornerRadius::from(radius)
|
||||||
|
};
|
||||||
|
let radius = radius.fit_to(rect.size.w as f32, rect.size.h as f32);
|
||||||
|
tabs_left -= 1;
|
||||||
|
|
||||||
|
shader.update(
|
||||||
|
rect.size,
|
||||||
|
gradient_area,
|
||||||
|
tab.gradient.in_,
|
||||||
|
color_from,
|
||||||
|
color_to,
|
||||||
|
((tab.gradient.angle as f32) - 90.).to_radians(),
|
||||||
|
Rectangle::from_size(rect.size),
|
||||||
|
0.,
|
||||||
|
radius,
|
||||||
|
scale as f32,
|
||||||
|
1.,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hit(
|
||||||
|
&self,
|
||||||
|
area: Rectangle<f64, Logical>,
|
||||||
|
tab_count: usize,
|
||||||
|
scale: f64,
|
||||||
|
point: Point<f64, Logical>,
|
||||||
|
) -> Option<usize> {
|
||||||
|
if self.config.off {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = tab_count;
|
||||||
|
if self.config.hide_when_single_tab && count == 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tab_rects(area, count, scale)
|
||||||
|
.enumerate()
|
||||||
|
.find_map(|(idx, rect)| rect.contains(point).then_some(idx))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(
|
||||||
|
&self,
|
||||||
|
renderer: &mut impl NiriRenderer,
|
||||||
|
pos: Point<f64, Logical>,
|
||||||
|
) -> impl Iterator<Item = TabIndicatorRenderElement> + '_ {
|
||||||
|
let has_border_shader = BorderRenderElement::has_shader(renderer);
|
||||||
|
if !has_border_shader {
|
||||||
|
return None.into_iter().flatten();
|
||||||
|
}
|
||||||
|
|
||||||
|
let rv = zip(&self.shaders, &self.shader_locs)
|
||||||
|
.map(move |(shader, loc)| shader.clone().with_location(pos + *loc))
|
||||||
|
.map(TabIndicatorRenderElement::from);
|
||||||
|
|
||||||
|
Some(rv).into_iter().flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extra size occupied by the tab indicator.
|
||||||
|
pub fn extra_size(&self, tab_count: usize, scale: f64) -> Size<f64, Logical> {
|
||||||
|
if self.config.off
|
||||||
|
|| !self.config.place_within_column
|
||||||
|
|| (self.config.hide_when_single_tab && tab_count == 1)
|
||||||
|
{
|
||||||
|
return Size::from((0., 0.));
|
||||||
|
}
|
||||||
|
|
||||||
|
let round = |logical: f64| round_logical_in_physical(scale, logical);
|
||||||
|
let width = round(self.config.width.0);
|
||||||
|
let gap = round(self.config.gap.0);
|
||||||
|
|
||||||
|
// No, I am *not* falling into the rabbit hole of "what if the tab indicator is wide enough
|
||||||
|
// that it peeks from the other side of the window".
|
||||||
|
let size = f64::max(0., width + gap);
|
||||||
|
|
||||||
|
match self.config.position {
|
||||||
|
TabIndicatorPosition::Left | TabIndicatorPosition::Right => Size::from((size, 0.)),
|
||||||
|
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => Size::from((0., size)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Offset of the tabbed content due to space occupied by the tab indicator.
|
||||||
|
pub fn content_offset(&self, tab_count: usize, scale: f64) -> Point<f64, Logical> {
|
||||||
|
match self.config.position {
|
||||||
|
TabIndicatorPosition::Left | TabIndicatorPosition::Top => {
|
||||||
|
self.extra_size(tab_count, scale).to_point()
|
||||||
|
}
|
||||||
|
TabIndicatorPosition::Right | TabIndicatorPosition::Bottom => Point::from((0., 0.)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config(&self) -> niri_config::TabIndicator {
|
||||||
|
self.config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TabInfo {
|
||||||
|
pub fn from_tile<W: LayoutElement>(
|
||||||
|
tile: &Tile<W>,
|
||||||
|
position: Point<f64, Logical>,
|
||||||
|
is_active: bool,
|
||||||
|
is_urgent: bool,
|
||||||
|
config: &niri_config::TabIndicator,
|
||||||
|
) -> Self {
|
||||||
|
let rules = tile.window().rules();
|
||||||
|
let rule = rules.tab_indicator;
|
||||||
|
|
||||||
|
let gradient_from_rule = || {
|
||||||
|
let (color, gradient) = if is_urgent {
|
||||||
|
(rule.urgent_color, rule.urgent_gradient)
|
||||||
|
} else if is_active {
|
||||||
|
(rule.active_color, rule.active_gradient)
|
||||||
|
} else {
|
||||||
|
(rule.inactive_color, rule.inactive_gradient)
|
||||||
|
};
|
||||||
|
let color = color.map(Gradient::from);
|
||||||
|
gradient.or(color)
|
||||||
|
};
|
||||||
|
|
||||||
|
let gradient_from_config = || {
|
||||||
|
let (color, gradient) = if is_urgent {
|
||||||
|
(config.urgent_color, config.urgent_gradient)
|
||||||
|
} else if is_active {
|
||||||
|
(config.active_color, config.active_gradient)
|
||||||
|
} else {
|
||||||
|
(config.inactive_color, config.inactive_gradient)
|
||||||
|
};
|
||||||
|
let color = color.map(Gradient::from);
|
||||||
|
gradient.or(color)
|
||||||
|
};
|
||||||
|
|
||||||
|
let gradient_from_border = || {
|
||||||
|
// Come up with tab indicator gradient matching the focus ring or the border, whichever
|
||||||
|
// one is enabled.
|
||||||
|
let focus_ring_config = tile.focus_ring().config();
|
||||||
|
let border_config = tile.border().config();
|
||||||
|
let config = if focus_ring_config.off {
|
||||||
|
border_config
|
||||||
|
} else {
|
||||||
|
focus_ring_config
|
||||||
|
};
|
||||||
|
|
||||||
|
let (color, gradient) = if is_urgent {
|
||||||
|
(config.urgent_color, config.urgent_gradient)
|
||||||
|
} else if is_active {
|
||||||
|
(config.active_color, config.active_gradient)
|
||||||
|
} else {
|
||||||
|
(config.inactive_color, config.inactive_gradient)
|
||||||
|
};
|
||||||
|
gradient.unwrap_or_else(|| Gradient::from(color))
|
||||||
|
};
|
||||||
|
|
||||||
|
let gradient = gradient_from_rule()
|
||||||
|
.or_else(gradient_from_config)
|
||||||
|
.unwrap_or_else(gradient_from_border);
|
||||||
|
|
||||||
|
let geometry = Rectangle::new(position, tile.animated_tile_size());
|
||||||
|
|
||||||
|
TabInfo { gradient, geometry }
|
||||||
|
}
|
||||||
|
}
|
||||||
+3666
File diff suppressed because it is too large
Load Diff
+393
-126
@@ -1,29 +1,32 @@
|
|||||||
|
use core::f64;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use niri_config::{Color, CornerRadius, GradientInterpolation};
|
use niri_config::{Color, CornerRadius, GradientInterpolation};
|
||||||
use smithay::backend::allocator::Fourcc;
|
|
||||||
use smithay::backend::renderer::element::{Element, Kind};
|
use smithay::backend::renderer::element::{Element, Kind};
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
|
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
||||||
|
|
||||||
use super::focus_ring::{FocusRing, FocusRingRenderElement};
|
use super::focus_ring::{FocusRing, FocusRingRenderElement};
|
||||||
use super::opening_window::{OpenAnimation, OpeningWindowRenderElement};
|
use super::opening_window::{OpenAnimation, OpeningWindowRenderElement};
|
||||||
|
use super::shadow::Shadow;
|
||||||
use super::{
|
use super::{
|
||||||
LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options,
|
HitType, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options,
|
||||||
RESIZE_ANIMATION_THRESHOLD,
|
SizeFrac, RESIZE_ANIMATION_THRESHOLD,
|
||||||
};
|
};
|
||||||
use crate::animation::Animation;
|
use crate::animation::{Animation, Clock};
|
||||||
use crate::niri_render_elements;
|
use crate::niri_render_elements;
|
||||||
use crate::render_helpers::border::BorderRenderElement;
|
use crate::render_helpers::border::BorderRenderElement;
|
||||||
use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage};
|
use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage};
|
||||||
use crate::render_helpers::damage::ExtraDamage;
|
use crate::render_helpers::damage::ExtraDamage;
|
||||||
|
use crate::render_helpers::offscreen::{OffscreenBuffer, OffscreenRenderElement};
|
||||||
use crate::render_helpers::renderer::NiriRenderer;
|
use crate::render_helpers::renderer::NiriRenderer;
|
||||||
use crate::render_helpers::resize::ResizeRenderElement;
|
use crate::render_helpers::resize::ResizeRenderElement;
|
||||||
|
use crate::render_helpers::shadow::ShadowRenderElement;
|
||||||
use crate::render_helpers::snapshot::RenderSnapshot;
|
use crate::render_helpers::snapshot::RenderSnapshot;
|
||||||
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||||
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
|
use crate::render_helpers::RenderTarget;
|
||||||
use crate::utils::transaction::Transaction;
|
use crate::utils::transaction::Transaction;
|
||||||
|
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
|
||||||
|
|
||||||
/// Toplevel window with decorations.
|
/// Toplevel window with decorations.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -35,11 +38,11 @@ pub struct Tile<W: LayoutElement> {
|
|||||||
border: FocusRing,
|
border: FocusRing,
|
||||||
|
|
||||||
/// The focus ring around the window.
|
/// The focus ring around the window.
|
||||||
///
|
|
||||||
/// It's supposed to be on the Workspace, but for the sake of a nicer open animation it's
|
|
||||||
/// currently here.
|
|
||||||
focus_ring: FocusRing,
|
focus_ring: FocusRing,
|
||||||
|
|
||||||
|
/// The shadow around the window.
|
||||||
|
shadow: Shadow,
|
||||||
|
|
||||||
/// Whether this tile is fullscreen.
|
/// Whether this tile is fullscreen.
|
||||||
///
|
///
|
||||||
/// This will update only when the `window` actually goes fullscreen, rather than right away,
|
/// This will update only when the `window` actually goes fullscreen, rather than right away,
|
||||||
@@ -49,8 +52,27 @@ pub struct Tile<W: LayoutElement> {
|
|||||||
/// The black backdrop for fullscreen windows.
|
/// The black backdrop for fullscreen windows.
|
||||||
fullscreen_backdrop: SolidColorBuffer,
|
fullscreen_backdrop: SolidColorBuffer,
|
||||||
|
|
||||||
/// The size we were requested to fullscreen into.
|
/// Whether the tile should float upon unfullscreening.
|
||||||
fullscreen_size: Size<f64, Logical>,
|
pub(super) unfullscreen_to_floating: bool,
|
||||||
|
|
||||||
|
/// The size that the window should assume when going floating.
|
||||||
|
///
|
||||||
|
/// This is generally the last size the window had when it was floating. It can be unknown if
|
||||||
|
/// the window starts out in the tiling layout or fullscreen.
|
||||||
|
pub(super) floating_window_size: Option<Size<i32, Logical>>,
|
||||||
|
|
||||||
|
/// The position that the tile should assume when going floating, relative to the floating
|
||||||
|
/// space working area.
|
||||||
|
///
|
||||||
|
/// This is generally the last position the tile had when it was floating. It can be unknown if
|
||||||
|
/// the window starts out in the tiling layout.
|
||||||
|
pub(super) floating_pos: Option<Point<f64, SizeFrac>>,
|
||||||
|
|
||||||
|
/// Currently selected preset width index when this tile is floating.
|
||||||
|
pub(super) floating_preset_width_idx: Option<usize>,
|
||||||
|
|
||||||
|
/// Currently selected preset height index when this tile is floating.
|
||||||
|
pub(super) floating_preset_height_idx: Option<usize>,
|
||||||
|
|
||||||
/// The animation upon opening a window.
|
/// The animation upon opening a window.
|
||||||
open_animation: Option<OpenAnimation>,
|
open_animation: Option<OpenAnimation>,
|
||||||
@@ -64,6 +86,9 @@ pub struct Tile<W: LayoutElement> {
|
|||||||
/// The animation of a tile visually moving vertically.
|
/// The animation of a tile visually moving vertically.
|
||||||
move_y_animation: Option<MoveAnimation>,
|
move_y_animation: Option<MoveAnimation>,
|
||||||
|
|
||||||
|
/// The animation of the tile's opacity.
|
||||||
|
pub(super) alpha_animation: Option<AlphaAnimation>,
|
||||||
|
|
||||||
/// Offset during the initial interactive move rubberband.
|
/// Offset during the initial interactive move rubberband.
|
||||||
pub(super) interactive_move_offset: Point<f64, Logical>,
|
pub(super) interactive_move_offset: Point<f64, Logical>,
|
||||||
|
|
||||||
@@ -73,9 +98,17 @@ pub struct Tile<W: LayoutElement> {
|
|||||||
/// Extra damage for clipped surface corner radius changes.
|
/// Extra damage for clipped surface corner radius changes.
|
||||||
rounded_corner_damage: RoundedCornerDamage,
|
rounded_corner_damage: RoundedCornerDamage,
|
||||||
|
|
||||||
|
/// The view size for the tile's workspace.
|
||||||
|
///
|
||||||
|
/// Used as the fullscreen target size.
|
||||||
|
view_size: Size<f64, Logical>,
|
||||||
|
|
||||||
/// Scale of the output the tile is on (and rounds its sizes to).
|
/// Scale of the output the tile is on (and rounds its sizes to).
|
||||||
scale: f64,
|
scale: f64,
|
||||||
|
|
||||||
|
/// Clock for driving animations.
|
||||||
|
pub(super) clock: Clock,
|
||||||
|
|
||||||
/// Configurable properties of the layout.
|
/// Configurable properties of the layout.
|
||||||
pub(super) options: Rc<Options>,
|
pub(super) options: Rc<Options>,
|
||||||
}
|
}
|
||||||
@@ -88,7 +121,9 @@ niri_render_elements! {
|
|||||||
Opening = OpeningWindowRenderElement,
|
Opening = OpeningWindowRenderElement,
|
||||||
Resize = ResizeRenderElement,
|
Resize = ResizeRenderElement,
|
||||||
Border = BorderRenderElement,
|
Border = BorderRenderElement,
|
||||||
|
Shadow = ShadowRenderElement,
|
||||||
ClippedSurface = ClippedSurfaceRenderElement<R>,
|
ClippedSurface = ClippedSurfaceRenderElement<R>,
|
||||||
|
Offscreen = OffscreenRenderElement,
|
||||||
ExtraDamage = ExtraDamage,
|
ExtraDamage = ExtraDamage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,6 +136,7 @@ struct ResizeAnimation {
|
|||||||
anim: Animation,
|
anim: Animation,
|
||||||
size_from: Size<f64, Logical>,
|
size_from: Size<f64, Logical>,
|
||||||
snapshot: LayoutElementRenderSnapshot,
|
snapshot: LayoutElementRenderSnapshot,
|
||||||
|
offscreen: OffscreenBuffer,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -109,32 +145,74 @@ struct MoveAnimation {
|
|||||||
from: f64,
|
from: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(super) struct AlphaAnimation {
|
||||||
|
pub(super) anim: Animation,
|
||||||
|
/// Whether the animation should persist after it's done.
|
||||||
|
///
|
||||||
|
/// This is used by things like interactive move which need to animate alpha to
|
||||||
|
/// semitransparent, then hold it at semitransparent for a while, until the operation
|
||||||
|
/// completes.
|
||||||
|
pub(super) hold_after_done: bool,
|
||||||
|
offscreen: OffscreenBuffer,
|
||||||
|
}
|
||||||
|
|
||||||
impl<W: LayoutElement> Tile<W> {
|
impl<W: LayoutElement> Tile<W> {
|
||||||
pub fn new(window: W, scale: f64, options: Rc<Options>) -> Self {
|
pub fn new(
|
||||||
|
window: W,
|
||||||
|
view_size: Size<f64, Logical>,
|
||||||
|
scale: f64,
|
||||||
|
clock: Clock,
|
||||||
|
options: Rc<Options>,
|
||||||
|
) -> Self {
|
||||||
let rules = window.rules();
|
let rules = window.rules();
|
||||||
let border_config = rules.border.resolve_against(options.border);
|
let border_config = rules.border.resolve_against(options.border);
|
||||||
let focus_ring_config = rules.focus_ring.resolve_against(options.focus_ring.into());
|
let focus_ring_config = rules.focus_ring.resolve_against(options.focus_ring.into());
|
||||||
|
let shadow_config = rules.shadow.resolve_against(options.shadow);
|
||||||
|
let is_fullscreen = window.is_fullscreen();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
window,
|
window,
|
||||||
border: FocusRing::new(border_config.into()),
|
border: FocusRing::new(border_config.into()),
|
||||||
focus_ring: FocusRing::new(focus_ring_config.into()),
|
focus_ring: FocusRing::new(focus_ring_config.into()),
|
||||||
is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size.
|
shadow: Shadow::new(shadow_config),
|
||||||
fullscreen_backdrop: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
|
is_fullscreen,
|
||||||
fullscreen_size: Default::default(),
|
fullscreen_backdrop: SolidColorBuffer::new(view_size, [0., 0., 0., 1.]),
|
||||||
|
unfullscreen_to_floating: false,
|
||||||
|
floating_window_size: None,
|
||||||
|
floating_pos: None,
|
||||||
|
floating_preset_width_idx: None,
|
||||||
|
floating_preset_height_idx: None,
|
||||||
open_animation: None,
|
open_animation: None,
|
||||||
resize_animation: None,
|
resize_animation: None,
|
||||||
move_x_animation: None,
|
move_x_animation: None,
|
||||||
move_y_animation: None,
|
move_y_animation: None,
|
||||||
|
alpha_animation: None,
|
||||||
interactive_move_offset: Point::from((0., 0.)),
|
interactive_move_offset: Point::from((0., 0.)),
|
||||||
unmap_snapshot: None,
|
unmap_snapshot: None,
|
||||||
rounded_corner_damage: Default::default(),
|
rounded_corner_damage: Default::default(),
|
||||||
|
view_size,
|
||||||
scale,
|
scale,
|
||||||
|
clock,
|
||||||
options,
|
options,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_config(&mut self, scale: f64, options: Rc<Options>) {
|
pub fn update_config(
|
||||||
|
&mut self,
|
||||||
|
view_size: Size<f64, Logical>,
|
||||||
|
scale: f64,
|
||||||
|
options: Rc<Options>,
|
||||||
|
) {
|
||||||
|
// If preset widths or heights changed, clear our stored preset index.
|
||||||
|
if self.options.preset_column_widths != options.preset_column_widths {
|
||||||
|
self.floating_preset_width_idx = None;
|
||||||
|
}
|
||||||
|
if self.options.preset_window_heights != options.preset_window_heights {
|
||||||
|
self.floating_preset_height_idx = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.view_size = view_size;
|
||||||
self.scale = scale;
|
self.scale = scale;
|
||||||
self.options = options;
|
self.options = options;
|
||||||
|
|
||||||
@@ -147,21 +225,24 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
.focus_ring
|
.focus_ring
|
||||||
.resolve_against(self.options.focus_ring.into());
|
.resolve_against(self.options.focus_ring.into());
|
||||||
self.focus_ring.update_config(focus_ring_config.into());
|
self.focus_ring.update_config(focus_ring_config.into());
|
||||||
|
|
||||||
|
let shadow_config = rules.shadow.resolve_against(self.options.shadow);
|
||||||
|
self.shadow.update_config(shadow_config);
|
||||||
|
|
||||||
|
self.fullscreen_backdrop.resize(view_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_shaders(&mut self) {
|
pub fn update_shaders(&mut self) {
|
||||||
self.border.update_shaders();
|
self.border.update_shaders();
|
||||||
self.focus_ring.update_shaders();
|
self.focus_ring.update_shaders();
|
||||||
|
self.shadow.update_shaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_window(&mut self) {
|
pub fn update_window(&mut self) {
|
||||||
// FIXME: remove when we can get a fullscreen size right away.
|
self.is_fullscreen = self.window.is_fullscreen();
|
||||||
if self.fullscreen_size != Size::from((0., 0.)) {
|
|
||||||
self.is_fullscreen = self.window.is_fullscreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(animate_from) = self.window.take_animation_snapshot() {
|
if let Some(animate_from) = self.window.take_animation_snapshot() {
|
||||||
let size_from = if let Some(resize) = self.resize_animation.take() {
|
let (size_from, offscreen) = if let Some(resize) = self.resize_animation.take() {
|
||||||
// Compute like in animated_window_size(), but using the snapshot geometry (since
|
// Compute like in animated_window_size(), but using the snapshot geometry (since
|
||||||
// the current one is already overwritten).
|
// the current one is already overwritten).
|
||||||
let mut size = animate_from.size;
|
let mut size = animate_from.size;
|
||||||
@@ -172,19 +253,27 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
size.w = size_from.w + (size.w - size_from.w) * val;
|
size.w = size_from.w + (size.w - size_from.w) * val;
|
||||||
size.h = size_from.h + (size.h - size_from.h) * val;
|
size.h = size_from.h + (size.h - size_from.h) * val;
|
||||||
|
|
||||||
size
|
// Also try to reuse the existing offscreen buffer if we have one.
|
||||||
|
(size, resize.offscreen)
|
||||||
} else {
|
} else {
|
||||||
animate_from.size
|
(animate_from.size, OffscreenBuffer::default())
|
||||||
};
|
};
|
||||||
|
|
||||||
let change = self.window.size().to_f64().to_point() - size_from.to_point();
|
let change = self.window.size().to_f64().to_point() - size_from.to_point();
|
||||||
let change = f64::max(change.x.abs(), change.y.abs());
|
let change = f64::max(change.x.abs(), change.y.abs());
|
||||||
if change > RESIZE_ANIMATION_THRESHOLD {
|
if change > RESIZE_ANIMATION_THRESHOLD {
|
||||||
let anim = Animation::new(0., 1., 0., self.options.animations.window_resize.anim);
|
let anim = Animation::new(
|
||||||
|
self.clock.clone(),
|
||||||
|
0.,
|
||||||
|
1.,
|
||||||
|
0.,
|
||||||
|
self.options.animations.window_resize.anim,
|
||||||
|
);
|
||||||
self.resize_animation = Some(ResizeAnimation {
|
self.resize_animation = Some(ResizeAnimation {
|
||||||
anim,
|
anim,
|
||||||
size_from,
|
size_from,
|
||||||
snapshot: animate_from,
|
snapshot: animate_from,
|
||||||
|
offscreen,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
self.resize_animation = None;
|
self.resize_animation = None;
|
||||||
@@ -199,6 +288,9 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
.resolve_against(self.options.focus_ring.into());
|
.resolve_against(self.options.focus_ring.into());
|
||||||
self.focus_ring.update_config(focus_ring_config.into());
|
self.focus_ring.update_config(focus_ring_config.into());
|
||||||
|
|
||||||
|
let shadow_config = rules.shadow.resolve_against(self.options.shadow);
|
||||||
|
self.shadow.update_config(shadow_config);
|
||||||
|
|
||||||
let window_size = self.window_size();
|
let window_size = self.window_size();
|
||||||
let radius = rules
|
let radius = rules
|
||||||
.geometry_corner_radius
|
.geometry_corner_radius
|
||||||
@@ -208,43 +300,53 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
self.rounded_corner_damage.set_size(window_size);
|
self.rounded_corner_damage.set_size(window_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn advance_animations(&mut self, current_time: Duration) {
|
pub fn advance_animations(&mut self) {
|
||||||
if let Some(open) = &mut self.open_animation {
|
if let Some(open) = &mut self.open_animation {
|
||||||
open.advance_animations(current_time);
|
|
||||||
if open.is_done() {
|
if open.is_done() {
|
||||||
self.open_animation = None;
|
self.open_animation = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(resize) = &mut self.resize_animation {
|
if let Some(resize) = &mut self.resize_animation {
|
||||||
resize.anim.set_current_time(current_time);
|
|
||||||
if resize.anim.is_done() {
|
if resize.anim.is_done() {
|
||||||
self.resize_animation = None;
|
self.resize_animation = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(move_) = &mut self.move_x_animation {
|
if let Some(move_) = &mut self.move_x_animation {
|
||||||
move_.anim.set_current_time(current_time);
|
|
||||||
if move_.anim.is_done() {
|
if move_.anim.is_done() {
|
||||||
self.move_x_animation = None;
|
self.move_x_animation = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(move_) = &mut self.move_y_animation {
|
if let Some(move_) = &mut self.move_y_animation {
|
||||||
move_.anim.set_current_time(current_time);
|
|
||||||
if move_.anim.is_done() {
|
if move_.anim.is_done() {
|
||||||
self.move_y_animation = None;
|
self.move_y_animation = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(alpha) = &mut self.alpha_animation {
|
||||||
|
if !alpha.hold_after_done && alpha.anim.is_done() {
|
||||||
|
self.alpha_animation = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn are_animations_ongoing(&self) -> bool {
|
pub fn are_animations_ongoing(&self) -> bool {
|
||||||
|
self.are_transitions_ongoing() || self.window.rules().baba_is_float == Some(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn are_transitions_ongoing(&self) -> bool {
|
||||||
self.open_animation.is_some()
|
self.open_animation.is_some()
|
||||||
|| self.resize_animation.is_some()
|
|| self.resize_animation.is_some()
|
||||||
|| self.move_x_animation.is_some()
|
|| self.move_x_animation.is_some()
|
||||||
|| self.move_y_animation.is_some()
|
|| self.move_y_animation.is_some()
|
||||||
|
|| self
|
||||||
|
.alpha_animation
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|alpha| !alpha.anim.is_done())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) {
|
pub fn update_render_elements(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) {
|
||||||
let rules = self.window.rules();
|
let rules = self.window.rules();
|
||||||
|
|
||||||
let draw_border_with_background = rules
|
let draw_border_with_background = rules
|
||||||
@@ -264,12 +366,29 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
self.animated_window_size(),
|
self.animated_window_size(),
|
||||||
is_active,
|
is_active,
|
||||||
!draw_border_with_background,
|
!draw_border_with_background,
|
||||||
Rectangle::from_loc_and_size(
|
self.window.is_urgent(),
|
||||||
|
Rectangle::new(
|
||||||
view_rect.loc - Point::from((border_width, border_width)),
|
view_rect.loc - Point::from((border_width, border_width)),
|
||||||
view_rect.size,
|
view_rect.size,
|
||||||
),
|
),
|
||||||
radius,
|
radius,
|
||||||
self.scale,
|
self.scale,
|
||||||
|
1.,
|
||||||
|
);
|
||||||
|
|
||||||
|
let radius = if self.is_fullscreen {
|
||||||
|
CornerRadius::default()
|
||||||
|
} else if self.effective_border_width().is_some() {
|
||||||
|
radius
|
||||||
|
} else {
|
||||||
|
rules.geometry_corner_radius.unwrap_or_default()
|
||||||
|
};
|
||||||
|
self.shadow.update_render_elements(
|
||||||
|
self.animated_tile_size(),
|
||||||
|
is_active,
|
||||||
|
radius,
|
||||||
|
self.scale,
|
||||||
|
1.,
|
||||||
);
|
);
|
||||||
|
|
||||||
let draw_focus_ring_with_background = if self.effective_border_width().is_some() {
|
let draw_focus_ring_with_background = if self.effective_border_width().is_some() {
|
||||||
@@ -277,21 +396,16 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
} else {
|
} else {
|
||||||
draw_border_with_background
|
draw_border_with_background
|
||||||
};
|
};
|
||||||
let radius = if self.is_fullscreen {
|
let radius = radius.expanded_by(self.focus_ring.width() as f32);
|
||||||
CornerRadius::default()
|
|
||||||
} else if self.effective_border_width().is_some() {
|
|
||||||
radius
|
|
||||||
} else {
|
|
||||||
rules.geometry_corner_radius.unwrap_or_default()
|
|
||||||
}
|
|
||||||
.expanded_by(self.focus_ring.width() as f32);
|
|
||||||
self.focus_ring.update_render_elements(
|
self.focus_ring.update_render_elements(
|
||||||
self.animated_tile_size(),
|
self.animated_tile_size(),
|
||||||
is_active,
|
is_active,
|
||||||
!draw_focus_ring_with_background,
|
!draw_focus_ring_with_background,
|
||||||
|
self.window.is_urgent(),
|
||||||
view_rect,
|
view_rect,
|
||||||
radius,
|
radius,
|
||||||
self.scale,
|
self.scale,
|
||||||
|
1.,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +430,7 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
|
|
||||||
pub fn start_open_animation(&mut self) {
|
pub fn start_open_animation(&mut self) {
|
||||||
self.open_animation = Some(OpenAnimation::new(Animation::new(
|
self.open_animation = Some(OpenAnimation::new(Animation::new(
|
||||||
|
self.clock.clone(),
|
||||||
0.,
|
0.,
|
||||||
1.,
|
1.,
|
||||||
0.,
|
0.,
|
||||||
@@ -343,7 +458,7 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
let anim = self.move_x_animation.take().map(|move_| move_.anim);
|
let anim = self.move_x_animation.take().map(|move_| move_.anim);
|
||||||
let anim = anim
|
let anim = anim
|
||||||
.map(|anim| anim.restarted(1., 0., 0.))
|
.map(|anim| anim.restarted(1., 0., 0.))
|
||||||
.unwrap_or_else(|| Animation::new(1., 0., 0., config));
|
.unwrap_or_else(|| Animation::new(self.clock.clone(), 1., 0., 0., config));
|
||||||
|
|
||||||
self.move_x_animation = Some(MoveAnimation {
|
self.move_x_animation = Some(MoveAnimation {
|
||||||
anim,
|
anim,
|
||||||
@@ -362,7 +477,7 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
let anim = self.move_y_animation.take().map(|move_| move_.anim);
|
let anim = self.move_y_animation.take().map(|move_| move_.anim);
|
||||||
let anim = anim
|
let anim = anim
|
||||||
.map(|anim| anim.restarted(1., 0., 0.))
|
.map(|anim| anim.restarted(1., 0., 0.))
|
||||||
.unwrap_or_else(|| Animation::new(1., 0., 0., config));
|
.unwrap_or_else(|| Animation::new(self.clock.clone(), 1., 0., 0., config));
|
||||||
|
|
||||||
self.move_y_animation = Some(MoveAnimation {
|
self.move_y_animation = Some(MoveAnimation {
|
||||||
anim,
|
anim,
|
||||||
@@ -375,6 +490,39 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
self.move_y_animation = None;
|
self.move_y_animation = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn animate_alpha(&mut self, from: f64, to: f64, config: niri_config::Animation) {
|
||||||
|
let from = from.clamp(0., 1.);
|
||||||
|
let to = to.clamp(0., 1.);
|
||||||
|
|
||||||
|
let (current, offscreen) = if let Some(alpha) = self.alpha_animation.take() {
|
||||||
|
(alpha.anim.clamped_value(), alpha.offscreen)
|
||||||
|
} else {
|
||||||
|
(from, OffscreenBuffer::default())
|
||||||
|
};
|
||||||
|
|
||||||
|
self.alpha_animation = Some(AlphaAnimation {
|
||||||
|
anim: Animation::new(self.clock.clone(), current, to, 0., config),
|
||||||
|
hold_after_done: false,
|
||||||
|
offscreen,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_alpha_animates_to_1(&mut self) {
|
||||||
|
if let Some(alpha) = &self.alpha_animation {
|
||||||
|
if alpha.anim.to() != 1. {
|
||||||
|
// Cancel animation instead of starting a new one because the user likely wants to
|
||||||
|
// see the tile right away.
|
||||||
|
self.alpha_animation = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hold_alpha_animation_after_done(&mut self) {
|
||||||
|
if let Some(alpha) = &mut self.alpha_animation {
|
||||||
|
alpha.hold_after_done = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn window(&self) -> &W {
|
pub fn window(&self) -> &W {
|
||||||
&self.window
|
&self.window
|
||||||
}
|
}
|
||||||
@@ -383,10 +531,6 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
&mut self.window
|
&mut self.window
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_window(self) -> W {
|
|
||||||
self.window
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_fullscreen(&self) -> bool {
|
pub fn is_fullscreen(&self) -> bool {
|
||||||
self.is_fullscreen
|
self.is_fullscreen
|
||||||
}
|
}
|
||||||
@@ -411,7 +555,7 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
// In fullscreen, center the window in the given size.
|
// In fullscreen, center the window in the given size.
|
||||||
if self.is_fullscreen {
|
if self.is_fullscreen {
|
||||||
let window_size = self.window_size();
|
let window_size = self.window_size();
|
||||||
let target_size = self.fullscreen_size;
|
let target_size = self.view_size;
|
||||||
|
|
||||||
// Windows aren't supposed to be larger than the fullscreen size, but in case we get
|
// Windows aren't supposed to be larger than the fullscreen size, but in case we get
|
||||||
// one, leave it at the top-left as usual.
|
// one, leave it at the top-left as usual.
|
||||||
@@ -441,8 +585,27 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
if self.is_fullscreen {
|
if self.is_fullscreen {
|
||||||
// Normally we'd just return the fullscreen size here, but this makes things a bit
|
// Normally we'd just return the fullscreen size here, but this makes things a bit
|
||||||
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
|
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
|
||||||
size.w = f64::max(size.w, self.fullscreen_size.w);
|
size.w = f64::max(size.w, self.view_size.w);
|
||||||
size.h = f64::max(size.h, self.fullscreen_size.h);
|
size.h = f64::max(size.h, self.view_size.h);
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(width) = self.effective_border_width() {
|
||||||
|
size.w += width * 2.;
|
||||||
|
size.h += width * 2.;
|
||||||
|
}
|
||||||
|
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tile_expected_or_current_size(&self) -> Size<f64, Logical> {
|
||||||
|
let mut size = self.window_expected_or_current_size();
|
||||||
|
|
||||||
|
if self.is_fullscreen {
|
||||||
|
// Normally we'd just return the fullscreen size here, but this makes things a bit
|
||||||
|
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
|
||||||
|
size.w = f64::max(size.w, self.view_size.w);
|
||||||
|
size.h = f64::max(size.h, self.view_size.h);
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +625,16 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
size
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
fn animated_window_size(&self) -> Size<f64, Logical> {
|
pub fn window_expected_or_current_size(&self) -> Size<f64, Logical> {
|
||||||
|
let size = self.window.expected_size();
|
||||||
|
let mut size = size.unwrap_or_else(|| self.window.size()).to_f64();
|
||||||
|
size = size
|
||||||
|
.to_physical_precise_round(self.scale)
|
||||||
|
.to_logical(self.scale);
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn animated_window_size(&self) -> Size<f64, Logical> {
|
||||||
let mut size = self.window_size();
|
let mut size = self.window_size();
|
||||||
|
|
||||||
if let Some(resize) = &self.resize_animation {
|
if let Some(resize) = &self.resize_animation {
|
||||||
@@ -479,14 +651,14 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
size
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
fn animated_tile_size(&self) -> Size<f64, Logical> {
|
pub fn animated_tile_size(&self) -> Size<f64, Logical> {
|
||||||
let mut size = self.animated_window_size();
|
let mut size = self.animated_window_size();
|
||||||
|
|
||||||
if self.is_fullscreen {
|
if self.is_fullscreen {
|
||||||
// Normally we'd just return the fullscreen size here, but this makes things a bit
|
// Normally we'd just return the fullscreen size here, but this makes things a bit
|
||||||
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
|
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
|
||||||
size.w = f64::max(size.w, self.fullscreen_size.w);
|
size.w = f64::max(size.w, self.view_size.w);
|
||||||
size.h = f64::max(size.h, self.fullscreen_size.h);
|
size.h = f64::max(size.h, self.view_size.h);
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,16 +677,32 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
loc
|
loc
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_in_input_region(&self, mut point: Point<f64, Logical>) -> bool {
|
fn is_in_input_region(&self, mut point: Point<f64, Logical>) -> bool {
|
||||||
point -= self.window_loc().to_f64();
|
point -= self.window_loc().to_f64();
|
||||||
self.window.is_in_input_region(point)
|
self.window.is_in_input_region(point)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
|
fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
|
||||||
let activation_region = Rectangle::from_loc_and_size((0., 0.), self.tile_size());
|
let activation_region = Rectangle::from_size(self.tile_size());
|
||||||
activation_region.contains(point)
|
activation_region.contains(point)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hit(&self, point: Point<f64, Logical>) -> Option<HitType> {
|
||||||
|
let offset = self.bob_offset();
|
||||||
|
let point = point - offset;
|
||||||
|
|
||||||
|
if self.is_in_input_region(point) {
|
||||||
|
let win_pos = self.buf_loc() + offset;
|
||||||
|
Some(HitType::Input { win_pos })
|
||||||
|
} else if self.is_in_activation_region(point) {
|
||||||
|
Some(HitType::Activate {
|
||||||
|
is_tab_indicator: false,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn request_tile_size(
|
pub fn request_tile_size(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut size: Size<f64, Logical>,
|
mut size: Size<f64, Logical>,
|
||||||
@@ -532,7 +720,7 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
// round to avoid situations where proportionally-sized columns don't fit on the screen
|
// round to avoid situations where proportionally-sized columns don't fit on the screen
|
||||||
// exactly.
|
// exactly.
|
||||||
self.window
|
self.window
|
||||||
.request_size(size.to_i32_floor(), animate, transaction);
|
.request_size(size.to_i32_floor(), false, animate, transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tile_width_for_window_width(&self, size: f64) -> f64 {
|
pub fn tile_width_for_window_width(&self, size: f64) -> f64 {
|
||||||
@@ -567,16 +755,18 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_fullscreen(&mut self, size: Size<f64, Logical>) {
|
pub fn request_fullscreen(&mut self, animate: bool, transaction: Option<Transaction>) {
|
||||||
self.fullscreen_backdrop.resize(size);
|
self.window
|
||||||
self.fullscreen_size = size;
|
.request_size(self.view_size.to_i32_round(), true, animate, transaction);
|
||||||
self.window.request_fullscreen(size.to_i32_round());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn min_size(&self) -> Size<f64, Logical> {
|
pub fn min_size_nonfullscreen(&self) -> Size<f64, Logical> {
|
||||||
let mut size = self.window.min_size().to_f64();
|
let mut size = self.window.min_size().to_f64();
|
||||||
|
|
||||||
if let Some(width) = self.effective_border_width() {
|
// Can't go through effective_border_width() because we might be fullscreen.
|
||||||
|
if !self.border.is_off() {
|
||||||
|
let width = self.border.width();
|
||||||
|
|
||||||
size.w = f64::max(1., size.w);
|
size.w = f64::max(1., size.w);
|
||||||
size.h = f64::max(1., size.h);
|
size.h = f64::max(1., size.h);
|
||||||
|
|
||||||
@@ -587,10 +777,13 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
size
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn max_size(&self) -> Size<f64, Logical> {
|
pub fn max_size_nonfullscreen(&self) -> Size<f64, Logical> {
|
||||||
let mut size = self.window.max_size().to_f64();
|
let mut size = self.window.max_size().to_f64();
|
||||||
|
|
||||||
if let Some(width) = self.effective_border_width() {
|
// Can't go through effective_border_width() because we might be fullscreen.
|
||||||
|
if !self.border.is_off() {
|
||||||
|
let width = self.border.width();
|
||||||
|
|
||||||
if size.w > 0. {
|
if size.w > 0. {
|
||||||
size.w += width * 2.;
|
size.w += width * 2.;
|
||||||
}
|
}
|
||||||
@@ -602,6 +795,16 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
size
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn bob_offset(&self) -> Point<f64, Logical> {
|
||||||
|
if self.window.rules().baba_is_float != Some(true) {
|
||||||
|
return Point::from((0., 0.));
|
||||||
|
}
|
||||||
|
|
||||||
|
let y = baba_is_float_offset(self.clock.now(), self.view_size.h);
|
||||||
|
let y = round_logical_in_physical(self.scale, y);
|
||||||
|
Point::from((0., y))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn draw_border_with_background(&self) -> bool {
|
pub fn draw_border_with_background(&self) -> bool {
|
||||||
if self.effective_border_width().is_some() {
|
if self.effective_border_width().is_some() {
|
||||||
return false;
|
return false;
|
||||||
@@ -613,27 +816,38 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
.unwrap_or_else(|| !self.window.has_ssd())
|
.unwrap_or_else(|| !self.window.has_ssd())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_inner<R: NiriRenderer>(
|
fn render_inner<'a, R: NiriRenderer + 'a>(
|
||||||
&self,
|
&'a self,
|
||||||
renderer: &mut R,
|
renderer: &mut R,
|
||||||
location: Point<f64, Logical>,
|
location: Point<f64, Logical>,
|
||||||
scale: Scale<f64>,
|
|
||||||
focus_ring: bool,
|
focus_ring: bool,
|
||||||
target: RenderTarget,
|
target: RenderTarget,
|
||||||
) -> impl Iterator<Item = TileRenderElement<R>> {
|
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
|
||||||
let _span = tracy_client::span!("Tile::render_inner");
|
let _span = tracy_client::span!("Tile::render_inner");
|
||||||
|
|
||||||
let alpha = if self.is_fullscreen {
|
let scale = Scale::from(self.scale);
|
||||||
|
|
||||||
|
let win_alpha = if self.is_fullscreen || self.window.is_ignoring_opacity_window_rule() {
|
||||||
1.
|
1.
|
||||||
} else {
|
} else {
|
||||||
self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.)
|
self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This is here rather than in render_offset() because render_offset() is currently assumed
|
||||||
|
// by the code to be temporary. So, for example, interactive move will try to "grab" the
|
||||||
|
// tile at its current render offset and reset the render offset to zero by cancelling the
|
||||||
|
// tile move animations. On the other hand, bob_offset() is not resettable, so adding it in
|
||||||
|
// render_offset() would cause obvious animation glitches.
|
||||||
|
//
|
||||||
|
// This isn't to say that adding it here is perfect; indeed, it kind of breaks view_rect
|
||||||
|
// passed to update_render_elements(). But, it works well enough for what it is.
|
||||||
|
let location = location + self.bob_offset();
|
||||||
|
|
||||||
let window_loc = self.window_loc();
|
let window_loc = self.window_loc();
|
||||||
let window_size = self.window_size().to_f64();
|
let window_size = self.window_size().to_f64();
|
||||||
let animated_window_size = self.animated_window_size();
|
let animated_window_size = self.animated_window_size();
|
||||||
let window_render_loc = location + window_loc;
|
let window_render_loc = location + window_loc;
|
||||||
let area = Rectangle::from_loc_and_size(window_render_loc, animated_window_size);
|
let area = Rectangle::new(window_render_loc, animated_window_size);
|
||||||
|
|
||||||
let rules = self.window.rules();
|
let rules = self.window.rules();
|
||||||
let clip_to_geometry = !self.is_fullscreen && rules.clip_to_geometry == Some(true);
|
let clip_to_geometry = !self.is_fullscreen && rules.clip_to_geometry == Some(true);
|
||||||
@@ -647,7 +861,7 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
if let Some(resize) = &self.resize_animation {
|
if let Some(resize) = &self.resize_animation {
|
||||||
resize_popups = Some(
|
resize_popups = Some(
|
||||||
self.window
|
self.window
|
||||||
.render_popups(renderer, window_render_loc, scale, alpha, target)
|
.render_popups(renderer, window_render_loc, scale, win_alpha, target)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(Into::into),
|
.map(Into::into),
|
||||||
);
|
);
|
||||||
@@ -664,15 +878,11 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
target,
|
target,
|
||||||
);
|
);
|
||||||
|
|
||||||
let current = render_to_encompassing_texture(
|
let current = resize
|
||||||
gles_renderer,
|
.offscreen
|
||||||
scale,
|
.render(gles_renderer, scale, &window_elements)
|
||||||
Transform::Normal,
|
.map_err(|err| warn!("error rendering window to texture: {err:?}"))
|
||||||
Fourcc::Abgr8888,
|
.ok();
|
||||||
&window_elements,
|
|
||||||
)
|
|
||||||
.map_err(|err| warn!("error rendering window to texture: {err:?}"))
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// Clip blocked-out resizes unconditionally because they use solid color render
|
// Clip blocked-out resizes unconditionally because they use solid color render
|
||||||
// elements.
|
// elements.
|
||||||
@@ -685,7 +895,13 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
clip_to_geometry
|
clip_to_geometry
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some((texture_current, _sync_point, texture_current_geo)) = current {
|
if let Some((elem_current, _sync_point, mut data)) = current {
|
||||||
|
let texture_current = elem_current.texture().clone();
|
||||||
|
// The offset and size are computed in physical pixels and converted to
|
||||||
|
// logical with the same `scale`, so converting them back with rounding
|
||||||
|
// inside the geometry() call gives us the same physical result back.
|
||||||
|
let texture_current_geo = elem_current.geometry(scale);
|
||||||
|
|
||||||
let elem = ResizeRenderElement::new(
|
let elem = ResizeRenderElement::new(
|
||||||
area,
|
area,
|
||||||
scale,
|
scale,
|
||||||
@@ -697,12 +913,15 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
resize.anim.clamped_value().clamp(0., 1.) as f32,
|
resize.anim.clamped_value().clamp(0., 1.) as f32,
|
||||||
radius,
|
radius,
|
||||||
clip_to_geometry,
|
clip_to_geometry,
|
||||||
alpha,
|
win_alpha,
|
||||||
);
|
);
|
||||||
// FIXME: with split popups, this will use the resize element ID for
|
|
||||||
// popups, but we want the real IDs.
|
// We're drawing the resize shader, not the offscreen directly.
|
||||||
self.window
|
data.id = elem.id().clone();
|
||||||
.set_offscreen_element_id(Some(elem.id().clone()));
|
|
||||||
|
// This is not a problem for split popups as the code will look for them by
|
||||||
|
// original id when it doesn't find them on the offscreen.
|
||||||
|
self.window.set_offscreen_data(Some(data));
|
||||||
resize_shader = Some(elem.into());
|
resize_shader = Some(elem.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -714,12 +933,11 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
SolidColorRenderElement::from_buffer(
|
SolidColorRenderElement::from_buffer(
|
||||||
&fallback_buffer,
|
&fallback_buffer,
|
||||||
area.loc,
|
area.loc,
|
||||||
alpha,
|
win_alpha,
|
||||||
Kind::Unspecified,
|
Kind::Unspecified,
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
self.window.set_offscreen_element_id(None);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,9 +948,9 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
if resize_shader.is_none() && resize_fallback.is_none() {
|
if resize_shader.is_none() && resize_fallback.is_none() {
|
||||||
let window = self
|
let window = self
|
||||||
.window
|
.window
|
||||||
.render(renderer, window_render_loc, scale, alpha, target);
|
.render(renderer, window_render_loc, scale, win_alpha, target);
|
||||||
|
|
||||||
let geo = Rectangle::from_loc_and_size(window_render_loc, window_size);
|
let geo = Rectangle::new(window_render_loc, window_size);
|
||||||
let radius = radius.fit_to(window_size.w as f32, window_size.h as f32);
|
let radius = radius.fit_to(window_size.w as f32, window_size.h as f32);
|
||||||
|
|
||||||
let clip_shader = ClippedSurfaceRenderElement::shader(renderer).cloned();
|
let clip_shader = ClippedSurfaceRenderElement::shader(renderer).cloned();
|
||||||
@@ -774,15 +992,16 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
if radius != CornerRadius::default() && has_border_shader {
|
if radius != CornerRadius::default() && has_border_shader {
|
||||||
return BorderRenderElement::new(
|
return BorderRenderElement::new(
|
||||||
geo.size,
|
geo.size,
|
||||||
Rectangle::from_loc_and_size((0., 0.), geo.size),
|
Rectangle::from_size(geo.size),
|
||||||
GradientInterpolation::default(),
|
GradientInterpolation::default(),
|
||||||
Color::from_color32f(elem.color()),
|
Color::from_color32f(elem.color()),
|
||||||
Color::from_color32f(elem.color()),
|
Color::from_color32f(elem.color()),
|
||||||
0.,
|
0.,
|
||||||
Rectangle::from_loc_and_size((0., 0.), geo.size),
|
Rectangle::from_size(geo.size),
|
||||||
0.,
|
0.,
|
||||||
radius,
|
radius,
|
||||||
scale.x as f32,
|
scale.x as f32,
|
||||||
|
1.,
|
||||||
)
|
)
|
||||||
.with_location(geo.loc)
|
.with_location(geo.loc)
|
||||||
.into();
|
.into();
|
||||||
@@ -824,81 +1043,98 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
let rv = rv.chain(elem.into_iter().flatten());
|
let rv = rv.chain(elem.into_iter().flatten());
|
||||||
|
|
||||||
let elem = focus_ring.then(|| self.focus_ring.render(renderer, location).map(Into::into));
|
let elem = focus_ring.then(|| self.focus_ring.render(renderer, location).map(Into::into));
|
||||||
rv.chain(elem.into_iter().flatten())
|
let rv = rv.chain(elem.into_iter().flatten());
|
||||||
|
|
||||||
|
rv.chain(self.shadow.render(renderer, location).map(Into::into))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render<R: NiriRenderer>(
|
pub fn render<'a, R: NiriRenderer + 'a>(
|
||||||
&self,
|
&'a self,
|
||||||
renderer: &mut R,
|
renderer: &mut R,
|
||||||
location: Point<f64, Logical>,
|
location: Point<f64, Logical>,
|
||||||
scale: Scale<f64>,
|
|
||||||
focus_ring: bool,
|
focus_ring: bool,
|
||||||
target: RenderTarget,
|
target: RenderTarget,
|
||||||
) -> impl Iterator<Item = TileRenderElement<R>> {
|
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
|
||||||
let _span = tracy_client::span!("Tile::render");
|
let _span = tracy_client::span!("Tile::render");
|
||||||
|
|
||||||
|
let scale = Scale::from(self.scale);
|
||||||
|
|
||||||
|
let tile_alpha = self
|
||||||
|
.alpha_animation
|
||||||
|
.as_ref()
|
||||||
|
.map_or(1., |alpha| alpha.anim.clamped_value()) as f32;
|
||||||
|
|
||||||
let mut open_anim_elem = None;
|
let mut open_anim_elem = None;
|
||||||
|
let mut alpha_anim_elem = None;
|
||||||
let mut window_elems = None;
|
let mut window_elems = None;
|
||||||
|
|
||||||
|
self.window().set_offscreen_data(None);
|
||||||
|
|
||||||
if let Some(open) = &self.open_animation {
|
if let Some(open) = &self.open_animation {
|
||||||
let renderer = renderer.as_gles_renderer();
|
let renderer = renderer.as_gles_renderer();
|
||||||
let elements =
|
let elements = self.render_inner(renderer, Point::from((0., 0.)), focus_ring, target);
|
||||||
self.render_inner(renderer, Point::from((0., 0.)), scale, focus_ring, target);
|
|
||||||
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
|
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
|
||||||
match open.render(renderer, &elements, self.tile_size(), location, scale) {
|
match open.render(
|
||||||
Ok(elem) => {
|
renderer,
|
||||||
self.window()
|
&elements,
|
||||||
.set_offscreen_element_id(Some(elem.id().clone()));
|
self.animated_tile_size(),
|
||||||
|
location,
|
||||||
|
scale,
|
||||||
|
tile_alpha,
|
||||||
|
) {
|
||||||
|
Ok((elem, data)) => {
|
||||||
|
self.window().set_offscreen_data(Some(data));
|
||||||
open_anim_elem = Some(elem.into());
|
open_anim_elem = Some(elem.into());
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("error rendering window opening animation: {err:?}");
|
warn!("error rendering window opening animation: {err:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if let Some(alpha) = &self.alpha_animation {
|
||||||
|
let renderer = renderer.as_gles_renderer();
|
||||||
|
let elements = self.render_inner(renderer, Point::from((0., 0.)), focus_ring, target);
|
||||||
|
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
|
||||||
|
match alpha.offscreen.render(renderer, scale, &elements) {
|
||||||
|
Ok((elem, _sync, data)) => {
|
||||||
|
let offset = elem.offset();
|
||||||
|
let elem = elem.with_alpha(tile_alpha).with_offset(location + offset);
|
||||||
|
|
||||||
|
self.window().set_offscreen_data(Some(data));
|
||||||
|
alpha_anim_elem = Some(elem.into());
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error rendering tile to offscreen for alpha animation: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if open_anim_elem.is_none() {
|
if open_anim_elem.is_none() && alpha_anim_elem.is_none() {
|
||||||
self.window().set_offscreen_element_id(None);
|
window_elems = Some(self.render_inner(renderer, location, focus_ring, target));
|
||||||
window_elems = Some(self.render_inner(renderer, location, scale, focus_ring, target));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open_anim_elem
|
open_anim_elem
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.chain(alpha_anim_elem)
|
||||||
.chain(window_elems.into_iter().flatten())
|
.chain(window_elems.into_iter().flatten())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store_unmap_snapshot_if_empty(
|
pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer) {
|
||||||
&mut self,
|
|
||||||
renderer: &mut GlesRenderer,
|
|
||||||
scale: Scale<f64>,
|
|
||||||
) {
|
|
||||||
if self.unmap_snapshot.is_some() {
|
if self.unmap_snapshot.is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.unmap_snapshot = Some(self.render_snapshot(renderer, scale));
|
self.unmap_snapshot = Some(self.render_snapshot(renderer));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_snapshot(
|
fn render_snapshot(&self, renderer: &mut GlesRenderer) -> TileRenderSnapshot {
|
||||||
&self,
|
|
||||||
renderer: &mut GlesRenderer,
|
|
||||||
scale: Scale<f64>,
|
|
||||||
) -> TileRenderSnapshot {
|
|
||||||
let _span = tracy_client::span!("Tile::render_snapshot");
|
let _span = tracy_client::span!("Tile::render_snapshot");
|
||||||
|
|
||||||
let contents = self.render(
|
let contents = self.render(renderer, Point::from((0., 0.)), false, RenderTarget::Output);
|
||||||
renderer,
|
|
||||||
Point::from((0., 0.)),
|
|
||||||
scale,
|
|
||||||
false,
|
|
||||||
RenderTarget::Output,
|
|
||||||
);
|
|
||||||
|
|
||||||
// A bit of a hack to render blocked out as for screencast, but I think it's fine here.
|
// A bit of a hack to render blocked out as for screencast, but I think it's fine here.
|
||||||
let blocked_out_contents = self.render(
|
let blocked_out_contents = self.render(
|
||||||
renderer,
|
renderer,
|
||||||
Point::from((0., 0.)),
|
Point::from((0., 0.)),
|
||||||
scale,
|
|
||||||
false,
|
false,
|
||||||
RenderTarget::Screencast,
|
RenderTarget::Screencast,
|
||||||
);
|
);
|
||||||
@@ -916,4 +1152,35 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
pub fn take_unmap_snapshot(&mut self) -> Option<TileRenderSnapshot> {
|
pub fn take_unmap_snapshot(&mut self) -> Option<TileRenderSnapshot> {
|
||||||
self.unmap_snapshot.take()
|
self.unmap_snapshot.take()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn border(&self) -> &FocusRing {
|
||||||
|
&self.border
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_ring(&self) -> &FocusRing {
|
||||||
|
&self.focus_ring
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn options(&self) -> &Rc<Options> {
|
||||||
|
&self.options
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn view_size(&self) -> Size<f64, Logical> {
|
||||||
|
self.view_size
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn verify_invariants(&self) {
|
||||||
|
use approx::assert_abs_diff_eq;
|
||||||
|
|
||||||
|
assert_eq!(self.is_fullscreen, self.window.is_fullscreen());
|
||||||
|
assert_eq!(self.fullscreen_backdrop.size(), self.view_size);
|
||||||
|
|
||||||
|
let scale = self.scale;
|
||||||
|
let size = self.tile_size();
|
||||||
|
let rounded = size.to_physical_precise_round(scale).to_logical(scale);
|
||||||
|
assert_abs_diff_eq!(size.w, rounded.w, epsilon = 1e-5);
|
||||||
|
assert_abs_diff_eq!(size.h, rounded.h, epsilon = 1e-5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1276
-3632
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ pub mod frame_clock;
|
|||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod ipc;
|
pub mod ipc;
|
||||||
|
pub mod layer;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod niri;
|
pub mod niri;
|
||||||
pub mod protocols;
|
pub mod protocols;
|
||||||
@@ -27,3 +28,6 @@ pub mod pw_utils;
|
|||||||
|
|
||||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||||
pub use dummy_pw_utils as pw_utils;
|
pub use dummy_pw_utils as pw_utils;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|||||||
+44
-37
@@ -5,13 +5,12 @@ use std::fmt::Write as _;
|
|||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::os::fd::FromRawFd;
|
use std::os::fd::FromRawFd;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::{env, mem};
|
use std::{env, mem};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::{CommandFactory, Parser};
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
use niri::animation;
|
|
||||||
use niri::cli::{Cli, Sub};
|
use niri::cli::{Cli, Sub};
|
||||||
#[cfg(feature = "dbus")]
|
#[cfg(feature = "dbus")]
|
||||||
use niri::dbus;
|
use niri::dbus;
|
||||||
@@ -49,6 +48,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
REMOVE_ENV_RUST_LIB_BACKTRACE.store(true, Ordering::Relaxed);
|
REMOVE_ENV_RUST_LIB_BACKTRACE.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
.with_writer(io::stderr)
|
||||||
|
.with_env_filter(env_filter)
|
||||||
|
.init();
|
||||||
|
|
||||||
if env::var_os("NOTIFY_SOCKET").is_some() {
|
if env::var_os("NOTIFY_SOCKET").is_some() {
|
||||||
IS_SYSTEMD_SERVICE.store(true, Ordering::Relaxed);
|
IS_SYSTEMD_SERVICE.store(true, Ordering::Relaxed);
|
||||||
|
|
||||||
@@ -59,19 +66,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
|
||||||
.with_env_filter(env_filter)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
if cli.session {
|
if cli.session {
|
||||||
// If we're starting as a session, assume that the intention is to start on a TTY. Remove
|
// If we're starting as a session, assume that the intention is to start on a TTY. Remove
|
||||||
// DISPLAY or WAYLAND_DISPLAY from our environment if they are set, since they will cause
|
// DISPLAY, WAYLAND_DISPLAY or WAYLAND_SOCKET from our environment if they are set, since
|
||||||
// the winit backend to be selected instead.
|
// they will cause the winit backend to be selected instead.
|
||||||
if env::var_os("DISPLAY").is_some() {
|
if env::var_os("DISPLAY").is_some() {
|
||||||
warn!("running as a session but DISPLAY is set, removing it");
|
warn!("running as a session but DISPLAY is set, removing it");
|
||||||
env::remove_var("DISPLAY");
|
env::remove_var("DISPLAY");
|
||||||
@@ -80,6 +80,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
warn!("running as a session but WAYLAND_DISPLAY is set, removing it");
|
warn!("running as a session but WAYLAND_DISPLAY is set, removing it");
|
||||||
env::remove_var("WAYLAND_DISPLAY");
|
env::remove_var("WAYLAND_DISPLAY");
|
||||||
}
|
}
|
||||||
|
if env::var_os("WAYLAND_SOCKET").is_some() {
|
||||||
|
warn!("running as a session but WAYLAND_SOCKET is set, removing it");
|
||||||
|
env::remove_var("WAYLAND_SOCKET");
|
||||||
|
}
|
||||||
|
|
||||||
// Set the current desktop for xdg-desktop-portal.
|
// Set the current desktop for xdg-desktop-portal.
|
||||||
env::set_var("XDG_CURRENT_DESKTOP", "niri");
|
env::set_var("XDG_CURRENT_DESKTOP", "niri");
|
||||||
@@ -87,9 +91,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
env::set_var("XDG_SESSION_TYPE", "wayland");
|
env::set_var("XDG_SESSION_TYPE", "wayland");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a better error printer for config loading.
|
|
||||||
niri_config::set_miette_hook().unwrap();
|
|
||||||
|
|
||||||
// Handle subcommands.
|
// Handle subcommands.
|
||||||
if let Some(subcommand) = cli.subcommand {
|
if let Some(subcommand) = cli.subcommand {
|
||||||
match subcommand {
|
match subcommand {
|
||||||
@@ -106,6 +107,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Sub::Panic => cause_panic(),
|
Sub::Panic => cause_panic(),
|
||||||
|
Sub::Completions { shell } => {
|
||||||
|
clap_complete::generate(shell, &mut Cli::command(), "niri", &mut io::stdout());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,21 +161,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut config_errored = false;
|
let config_load_result = Config::load(&path);
|
||||||
let mut config = Config::load(&path)
|
let config_errored = config_load_result.is_err();
|
||||||
.map_err(|err| {
|
let mut config = config_load_result
|
||||||
warn!("{err:?}");
|
.map_err(|err| warn!("{err:?}"))
|
||||||
config_errored = true;
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
.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);
|
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
|
||||||
*CHILD_ENV.write().unwrap() = mem::take(&mut config.environment);
|
*CHILD_ENV.write().unwrap() = mem::take(&mut config.environment);
|
||||||
|
|
||||||
@@ -184,11 +180,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
event_loop.handle(),
|
event_loop.handle(),
|
||||||
event_loop.get_signal(),
|
event_loop.get_signal(),
|
||||||
display,
|
display,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Set WAYLAND_DISPLAY for children.
|
// Set WAYLAND_DISPLAY for children.
|
||||||
let socket_name = &state.niri.socket_name;
|
let socket_name = state.niri.socket_name.as_deref().unwrap();
|
||||||
env::set_var("WAYLAND_DISPLAY", socket_name);
|
env::set_var("WAYLAND_DISPLAY", socket_name);
|
||||||
info!(
|
info!(
|
||||||
"listening on Wayland socket: {}",
|
"listening on Wayland socket: {}",
|
||||||
@@ -197,8 +195,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// Set NIRI_SOCKET for children.
|
// Set NIRI_SOCKET for children.
|
||||||
if let Some(ipc) = &state.niri.ipc_server {
|
if let Some(ipc) = &state.niri.ipc_server {
|
||||||
env::set_var(SOCKET_PATH_ENV, &ipc.socket_path);
|
let socket_path = ipc.socket_path.as_deref().unwrap();
|
||||||
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
|
env::set_var(SOCKET_PATH_ENV, socket_path);
|
||||||
|
info!("IPC listening on: {}", socket_path.to_string_lossy());
|
||||||
}
|
}
|
||||||
|
|
||||||
if cli.session {
|
if cli.session {
|
||||||
@@ -231,12 +230,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// Set up config file watcher.
|
// Set up config file watcher.
|
||||||
let _watcher = {
|
let _watcher = {
|
||||||
|
// Parsing the config actually takes > 20 ms on my beefy machine, so let's do it on the
|
||||||
|
// watcher thread.
|
||||||
|
let process = |path: &Path| {
|
||||||
|
Config::load(path).map_err(|err| {
|
||||||
|
warn!("{:?}", err.context("error loading config"));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
let (tx, rx) = calloop::channel::sync_channel(1);
|
let (tx, rx) = calloop::channel::sync_channel(1);
|
||||||
let watcher = Watcher::new(watch_path.clone(), tx);
|
let watcher = Watcher::new(watch_path.clone(), process, tx);
|
||||||
event_loop
|
event_loop
|
||||||
.handle()
|
.handle()
|
||||||
.insert_source(rx, move |event, _, state| match event {
|
.insert_source(rx, |event, _, state| match event {
|
||||||
calloop::channel::Event::Msg(()) => state.reload_config(watch_path.clone()),
|
calloop::channel::Event::Msg(config) => state.reload_config(config),
|
||||||
calloop::channel::Event::Closed => (),
|
calloop::channel::Event::Closed => (),
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -244,10 +251,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Spawn commands from cli and auto-start.
|
// Spawn commands from cli and auto-start.
|
||||||
spawn(cli.command);
|
spawn(cli.command, None);
|
||||||
|
|
||||||
for elem in spawn_at_startup {
|
for elem in spawn_at_startup {
|
||||||
spawn(elem.command);
|
spawn(elem.command, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show the config error notification right away if needed.
|
// Show the config error notification right away if needed.
|
||||||
@@ -346,7 +353,7 @@ fn config_path(cli_path: Option<PathBuf>) -> (PathBuf, PathBuf, bool) {
|
|||||||
let system_path = system_config_path();
|
let system_path = system_config_path();
|
||||||
if let Some(path) = default_config_path() {
|
if let Some(path) = default_config_path() {
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
return (path.clone(), path, true);
|
return (path.clone(), path, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if system_path.exists() {
|
if system_path.exists() {
|
||||||
|
|||||||
+1824
-579
File diff suppressed because it is too large
Load Diff
@@ -177,7 +177,6 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify that there's no more data.
|
// Verify that there's no more data.
|
||||||
#[allow(clippy::unused_io_amount)] // False positive on 1.77.0
|
|
||||||
{
|
{
|
||||||
match file.read(&mut [0]) {
|
match file.read(&mut [0]) {
|
||||||
Ok(0) => (),
|
Ok(0) => (),
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ pub mod gamma_control;
|
|||||||
pub mod mutter_x11_interop;
|
pub mod mutter_x11_interop;
|
||||||
pub mod output_management;
|
pub mod output_management;
|
||||||
pub mod screencopy;
|
pub mod screencopy;
|
||||||
|
pub mod virtual_pointer;
|
||||||
|
|
||||||
pub mod raw;
|
pub mod raw;
|
||||||
|
|||||||
@@ -206,9 +206,9 @@ where
|
|||||||
let output_transform = output.current_transform();
|
let output_transform = output.current_transform();
|
||||||
let output_physical_size =
|
let output_physical_size =
|
||||||
output_transform.transform_size(output.current_mode().unwrap().size);
|
output_transform.transform_size(output.current_mode().unwrap().size);
|
||||||
let output_rect = Rectangle::from_loc_and_size((0, 0), output_physical_size);
|
let output_rect = Rectangle::from_size(output_physical_size);
|
||||||
|
|
||||||
let rect = Rectangle::from_loc_and_size((x, y), (width, height));
|
let rect = Rectangle::new(Point::from((x, y)), Size::from((width, height)));
|
||||||
|
|
||||||
let output_scale = output.current_scale().fractional_scale();
|
let output_scale = output.current_scale().fractional_scale();
|
||||||
let physical_rect = rect.to_physical_precise_round(output_scale);
|
let physical_rect = rect.to_physical_precise_round(output_scale);
|
||||||
|
|||||||
@@ -0,0 +1,563 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use smithay::backend::input::{
|
||||||
|
AbsolutePositionEvent, Axis, AxisRelativeDirection, AxisSource, ButtonState, Device,
|
||||||
|
DeviceCapability, Event, InputBackend, PointerAxisEvent, PointerButtonEvent,
|
||||||
|
PointerMotionAbsoluteEvent, PointerMotionEvent, UnusedEvent,
|
||||||
|
};
|
||||||
|
use smithay::input::pointer::AxisFrame;
|
||||||
|
use smithay::output::Output;
|
||||||
|
use smithay::reexports::wayland_protocols_wlr;
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_pointer;
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
|
||||||
|
use smithay::reexports::wayland_server::{
|
||||||
|
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||||
|
};
|
||||||
|
use wayland_backend::protocol::WEnum;
|
||||||
|
use wayland_protocols_wlr::virtual_pointer::v1::server::{
|
||||||
|
zwlr_virtual_pointer_manager_v1, zwlr_virtual_pointer_v1,
|
||||||
|
};
|
||||||
|
use zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1;
|
||||||
|
use zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1;
|
||||||
|
|
||||||
|
const VERSION: u32 = 2;
|
||||||
|
|
||||||
|
pub struct VirtualPointerManagerState {
|
||||||
|
virtual_pointers: HashSet<ZwlrVirtualPointerV1>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VirtualPointerManagerGlobalData {
|
||||||
|
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VirtualPointerInputBackend;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
|
pub struct VirtualPointer {
|
||||||
|
pointer: ZwlrVirtualPointerV1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VirtualPointerUserData {
|
||||||
|
seat: Option<WlSeat>,
|
||||||
|
output: Option<Output>,
|
||||||
|
|
||||||
|
axis_frame: Mutex<Option<AxisFrame>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VirtualPointer {
|
||||||
|
fn data(&self) -> &VirtualPointerUserData {
|
||||||
|
self.pointer.data().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seat(&self) -> Option<&WlSeat> {
|
||||||
|
self.data().seat.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output(&self) -> Option<&Output> {
|
||||||
|
self.data().output.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_axis_frame(&self) -> Option<AxisFrame> {
|
||||||
|
self.data().axis_frame.lock().unwrap().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mutate_axis_frame(&self, time: Option<u32>, f: impl FnOnce(AxisFrame) -> AxisFrame) {
|
||||||
|
let mut frame = self.data().axis_frame.lock().unwrap();
|
||||||
|
|
||||||
|
*frame = frame.or(time.map(AxisFrame::new)).map(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Device for VirtualPointer {
|
||||||
|
fn id(&self) -> String {
|
||||||
|
format!("wlr virtual pointer {}", self.pointer.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
String::from("virtual pointer")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_capability(&self, capability: DeviceCapability) -> bool {
|
||||||
|
matches!(capability, DeviceCapability::Pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usb_id(&self) -> Option<(u32, u32)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn syspath(&self) -> Option<std::path::PathBuf> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VirtualPointerMotionEvent {
|
||||||
|
pointer: VirtualPointer,
|
||||||
|
time: u32,
|
||||||
|
dx: f64,
|
||||||
|
dy: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event<VirtualPointerInputBackend> for VirtualPointerMotionEvent {
|
||||||
|
fn time(&self) -> u64 {
|
||||||
|
self.time as u64 * 1000 // millis to micros
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device(&self) -> VirtualPointer {
|
||||||
|
self.pointer.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerMotionEvent<VirtualPointerInputBackend> for VirtualPointerMotionEvent {
|
||||||
|
fn delta_x(&self) -> f64 {
|
||||||
|
self.dx
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delta_y(&self) -> f64 {
|
||||||
|
self.dy
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delta_x_unaccel(&self) -> f64 {
|
||||||
|
self.dx
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delta_y_unaccel(&self) -> f64 {
|
||||||
|
self.dy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VirtualPointerMotionAbsoluteEvent {
|
||||||
|
pointer: VirtualPointer,
|
||||||
|
time: u32,
|
||||||
|
x: u32,
|
||||||
|
y: u32,
|
||||||
|
x_extent: u32,
|
||||||
|
y_extent: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event<VirtualPointerInputBackend> for VirtualPointerMotionAbsoluteEvent {
|
||||||
|
fn time(&self) -> u64 {
|
||||||
|
self.time as u64 * 1000 // millis to micros
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device(&self) -> VirtualPointer {
|
||||||
|
self.pointer.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AbsolutePositionEvent<VirtualPointerInputBackend> for VirtualPointerMotionAbsoluteEvent {
|
||||||
|
fn x(&self) -> f64 {
|
||||||
|
self.x as f64 / self.x_extent as f64
|
||||||
|
}
|
||||||
|
|
||||||
|
fn y(&self) -> f64 {
|
||||||
|
self.y as f64 / self.y_extent as f64
|
||||||
|
}
|
||||||
|
|
||||||
|
fn x_transformed(&self, width: i32) -> f64 {
|
||||||
|
(self.x as i64 * width as i64) as f64 / self.x_extent as f64
|
||||||
|
}
|
||||||
|
|
||||||
|
fn y_transformed(&self, height: i32) -> f64 {
|
||||||
|
(self.y as i64 * height as i64) as f64 / self.y_extent as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VirtualPointerButtonEvent {
|
||||||
|
pointer: VirtualPointer,
|
||||||
|
time: u32,
|
||||||
|
button: u32,
|
||||||
|
state: ButtonState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event<VirtualPointerInputBackend> for VirtualPointerButtonEvent {
|
||||||
|
fn time(&self) -> u64 {
|
||||||
|
self.time as u64 * 1000 // millis to micros
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device(&self) -> VirtualPointer {
|
||||||
|
self.pointer.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerButtonEvent<VirtualPointerInputBackend> for VirtualPointerButtonEvent {
|
||||||
|
fn button_code(&self) -> u32 {
|
||||||
|
self.button
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> ButtonState {
|
||||||
|
self.state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VirtualPointerAxisEvent {
|
||||||
|
pointer: VirtualPointer,
|
||||||
|
frame: AxisFrame,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event<VirtualPointerInputBackend> for VirtualPointerAxisEvent {
|
||||||
|
fn time(&self) -> u64 {
|
||||||
|
self.frame.time as u64 * 1000 // millis to micros
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device(&self) -> VirtualPointer {
|
||||||
|
self.pointer.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tuple_axis<T>(tuple: (T, T), axis: Axis) -> T {
|
||||||
|
match axis {
|
||||||
|
Axis::Horizontal => tuple.0,
|
||||||
|
Axis::Vertical => tuple.1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerAxisEvent<VirtualPointerInputBackend> for VirtualPointerAxisEvent {
|
||||||
|
fn amount(&self, axis: Axis) -> Option<f64> {
|
||||||
|
Some(tuple_axis(self.frame.axis, axis))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn amount_v120(&self, axis: Axis) -> Option<f64> {
|
||||||
|
self.frame.v120.map(|v120| tuple_axis(v120, axis) as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source(&self) -> AxisSource {
|
||||||
|
self.frame.source.unwrap_or_else(|| {
|
||||||
|
warn!("AxisSource: no source set, giving bogus value");
|
||||||
|
AxisSource::Continuous
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relative_direction(&self, axis: Axis) -> AxisRelativeDirection {
|
||||||
|
tuple_axis(self.frame.relative_direction, axis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerMotionAbsoluteEvent<VirtualPointerInputBackend> for VirtualPointerMotionAbsoluteEvent {}
|
||||||
|
|
||||||
|
impl InputBackend for VirtualPointerInputBackend {
|
||||||
|
type Device = VirtualPointer;
|
||||||
|
|
||||||
|
type KeyboardKeyEvent = UnusedEvent;
|
||||||
|
type PointerAxisEvent = VirtualPointerAxisEvent;
|
||||||
|
type PointerButtonEvent = VirtualPointerButtonEvent;
|
||||||
|
type PointerMotionEvent = VirtualPointerMotionEvent;
|
||||||
|
type PointerMotionAbsoluteEvent = VirtualPointerMotionAbsoluteEvent;
|
||||||
|
|
||||||
|
type GestureSwipeBeginEvent = UnusedEvent;
|
||||||
|
type GestureSwipeUpdateEvent = UnusedEvent;
|
||||||
|
type GestureSwipeEndEvent = UnusedEvent;
|
||||||
|
type GesturePinchBeginEvent = UnusedEvent;
|
||||||
|
type GesturePinchUpdateEvent = UnusedEvent;
|
||||||
|
type GesturePinchEndEvent = UnusedEvent;
|
||||||
|
type GestureHoldBeginEvent = UnusedEvent;
|
||||||
|
type GestureHoldEndEvent = UnusedEvent;
|
||||||
|
|
||||||
|
type TouchDownEvent = UnusedEvent;
|
||||||
|
type TouchUpEvent = UnusedEvent;
|
||||||
|
type TouchMotionEvent = UnusedEvent;
|
||||||
|
type TouchCancelEvent = UnusedEvent;
|
||||||
|
type TouchFrameEvent = UnusedEvent;
|
||||||
|
type TabletToolAxisEvent = UnusedEvent;
|
||||||
|
type TabletToolProximityEvent = UnusedEvent;
|
||||||
|
type TabletToolTipEvent = UnusedEvent;
|
||||||
|
type TabletToolButtonEvent = UnusedEvent;
|
||||||
|
|
||||||
|
type SwitchToggleEvent = UnusedEvent;
|
||||||
|
|
||||||
|
type SpecialEvent = UnusedEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait VirtualPointerHandler {
|
||||||
|
fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState;
|
||||||
|
|
||||||
|
fn create_virtual_pointer(&mut self, pointer: VirtualPointer) {
|
||||||
|
let _ = pointer;
|
||||||
|
}
|
||||||
|
fn destroy_virtual_pointer(&mut self, pointer: VirtualPointer) {
|
||||||
|
let _ = pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_virtual_pointer_motion(&mut self, event: VirtualPointerMotionEvent);
|
||||||
|
fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent);
|
||||||
|
fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent);
|
||||||
|
fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VirtualPointerManagerState {
|
||||||
|
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
|
||||||
|
where
|
||||||
|
D: GlobalDispatch<ZwlrVirtualPointerManagerV1, VirtualPointerManagerGlobalData>,
|
||||||
|
D: Dispatch<ZwlrVirtualPointerManagerV1, ()>,
|
||||||
|
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
|
||||||
|
D: VirtualPointerHandler,
|
||||||
|
D: 'static,
|
||||||
|
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let global_data = VirtualPointerManagerGlobalData {
|
||||||
|
filter: Box::new(filter),
|
||||||
|
};
|
||||||
|
display.create_global::<D, ZwlrVirtualPointerManagerV1, _>(VERSION, global_data);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
virtual_pointers: HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> GlobalDispatch<ZwlrVirtualPointerManagerV1, VirtualPointerManagerGlobalData, D>
|
||||||
|
for VirtualPointerManagerState
|
||||||
|
where
|
||||||
|
D: GlobalDispatch<ZwlrVirtualPointerManagerV1, VirtualPointerManagerGlobalData>,
|
||||||
|
D: Dispatch<ZwlrVirtualPointerManagerV1, ()>,
|
||||||
|
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
|
||||||
|
D: VirtualPointerHandler,
|
||||||
|
D: 'static,
|
||||||
|
{
|
||||||
|
fn bind(
|
||||||
|
_state: &mut D,
|
||||||
|
_handle: &DisplayHandle,
|
||||||
|
_client: &Client,
|
||||||
|
manager: New<ZwlrVirtualPointerManagerV1>,
|
||||||
|
_manager_state: &VirtualPointerManagerGlobalData,
|
||||||
|
data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
data_init.init(manager, ());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_view(client: Client, global_data: &VirtualPointerManagerGlobalData) -> bool {
|
||||||
|
(global_data.filter)(&client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> Dispatch<ZwlrVirtualPointerManagerV1, (), D> for VirtualPointerManagerState
|
||||||
|
where
|
||||||
|
D: Dispatch<ZwlrVirtualPointerManagerV1, ()>,
|
||||||
|
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
|
||||||
|
D: VirtualPointerHandler,
|
||||||
|
D: 'static,
|
||||||
|
{
|
||||||
|
fn request(
|
||||||
|
state: &mut D,
|
||||||
|
_client: &Client,
|
||||||
|
_resource: &ZwlrVirtualPointerManagerV1,
|
||||||
|
request: <ZwlrVirtualPointerManagerV1 as Resource>::Request,
|
||||||
|
_data: &(),
|
||||||
|
_dhandle: &DisplayHandle,
|
||||||
|
data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
let (id, seat, output) = match request {
|
||||||
|
zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointer { seat, id } => {
|
||||||
|
(id, seat, None)
|
||||||
|
}
|
||||||
|
zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointerWithOutput {
|
||||||
|
seat,
|
||||||
|
output,
|
||||||
|
id,
|
||||||
|
} => (id, seat, output.as_ref().and_then(Output::from_resource)),
|
||||||
|
zwlr_virtual_pointer_manager_v1::Request::Destroy => return,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let pointer = data_init.init(
|
||||||
|
id,
|
||||||
|
VirtualPointerUserData {
|
||||||
|
seat,
|
||||||
|
output,
|
||||||
|
axis_frame: Mutex::new(None),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
state
|
||||||
|
.virtual_pointer_manager_state()
|
||||||
|
.virtual_pointers
|
||||||
|
.insert(pointer.clone());
|
||||||
|
|
||||||
|
state.create_virtual_pointer(VirtualPointer { pointer });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData, D> for VirtualPointerManagerState
|
||||||
|
where
|
||||||
|
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
|
||||||
|
D: VirtualPointerHandler,
|
||||||
|
D: 'static,
|
||||||
|
{
|
||||||
|
fn request(
|
||||||
|
handler: &mut D,
|
||||||
|
_client: &Client,
|
||||||
|
resource: &ZwlrVirtualPointerV1,
|
||||||
|
request: <ZwlrVirtualPointerV1 as Resource>::Request,
|
||||||
|
_data: &VirtualPointerUserData,
|
||||||
|
_dhandle: &DisplayHandle,
|
||||||
|
_data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
let pointer = VirtualPointer {
|
||||||
|
pointer: resource.clone(),
|
||||||
|
};
|
||||||
|
match request {
|
||||||
|
zwlr_virtual_pointer_v1::Request::Motion { time, dx, dy } => {
|
||||||
|
let event = VirtualPointerMotionEvent {
|
||||||
|
pointer,
|
||||||
|
time,
|
||||||
|
dx,
|
||||||
|
dy,
|
||||||
|
};
|
||||||
|
handler.on_virtual_pointer_motion(event);
|
||||||
|
}
|
||||||
|
zwlr_virtual_pointer_v1::Request::MotionAbsolute {
|
||||||
|
time,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
x_extent,
|
||||||
|
y_extent,
|
||||||
|
} => {
|
||||||
|
let event = VirtualPointerMotionAbsoluteEvent {
|
||||||
|
pointer,
|
||||||
|
time,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
x_extent,
|
||||||
|
y_extent,
|
||||||
|
};
|
||||||
|
handler.on_virtual_pointer_motion_absolute(event);
|
||||||
|
}
|
||||||
|
zwlr_virtual_pointer_v1::Request::Button {
|
||||||
|
time,
|
||||||
|
button,
|
||||||
|
state,
|
||||||
|
} => {
|
||||||
|
// state is an enum but wlroots treats it as a C boolean (zero or nonzero)
|
||||||
|
// so we emulate that behaviour too. ButtonState::Pressed and any invalid value
|
||||||
|
// counts as pressed.
|
||||||
|
// https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/3187479c07c34a4de82c06a316a763a36a0499da/types/wlr_virtual_pointer_v1.c#L74
|
||||||
|
let state = match state {
|
||||||
|
WEnum::Value(wl_pointer::ButtonState::Released) => ButtonState::Released,
|
||||||
|
_ => ButtonState::Pressed,
|
||||||
|
};
|
||||||
|
let event = VirtualPointerButtonEvent {
|
||||||
|
pointer,
|
||||||
|
time,
|
||||||
|
button,
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
handler.on_virtual_pointer_button(event);
|
||||||
|
}
|
||||||
|
zwlr_virtual_pointer_v1::Request::Axis { time, axis, value } => {
|
||||||
|
let axis = match axis {
|
||||||
|
WEnum::Value(wl_pointer::Axis::VerticalScroll) => Axis::Vertical,
|
||||||
|
WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Axis::Horizontal,
|
||||||
|
_ => {
|
||||||
|
warn!("Axis: invalid axis");
|
||||||
|
resource.post_error(
|
||||||
|
zwlr_virtual_pointer_v1::Error::InvalidAxis,
|
||||||
|
"invalid axis",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pointer.mutate_axis_frame(Some(time), |frame| frame.value(axis, value));
|
||||||
|
}
|
||||||
|
zwlr_virtual_pointer_v1::Request::Frame => {
|
||||||
|
if let Some(frame) = pointer.finish_axis_frame() {
|
||||||
|
let event = VirtualPointerAxisEvent { pointer, frame };
|
||||||
|
handler.on_virtual_pointer_axis(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zwlr_virtual_pointer_v1::Request::AxisSource { axis_source } => {
|
||||||
|
let axis_source = match axis_source {
|
||||||
|
WEnum::Value(wl_pointer::AxisSource::Wheel) => AxisSource::Wheel,
|
||||||
|
WEnum::Value(wl_pointer::AxisSource::Finger) => AxisSource::Finger,
|
||||||
|
WEnum::Value(wl_pointer::AxisSource::Continuous) => AxisSource::Continuous,
|
||||||
|
WEnum::Value(wl_pointer::AxisSource::WheelTilt) => AxisSource::WheelTilt,
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
warn!("AxisSource: invalid axis source");
|
||||||
|
resource.post_error(
|
||||||
|
zwlr_virtual_pointer_v1::Error::InvalidAxisSource,
|
||||||
|
"invalid axis source",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pointer.mutate_axis_frame(None, |frame| frame.source(axis_source));
|
||||||
|
}
|
||||||
|
zwlr_virtual_pointer_v1::Request::AxisStop { time, axis } => {
|
||||||
|
let axis = match axis {
|
||||||
|
WEnum::Value(wl_pointer::Axis::VerticalScroll) => Axis::Vertical,
|
||||||
|
WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Axis::Horizontal,
|
||||||
|
_ => {
|
||||||
|
warn!("AxisStop: invalid axis");
|
||||||
|
resource.post_error(
|
||||||
|
zwlr_virtual_pointer_v1::Error::InvalidAxis,
|
||||||
|
"invalid axis",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pointer.mutate_axis_frame(Some(time), |frame| frame.stop(axis));
|
||||||
|
}
|
||||||
|
zwlr_virtual_pointer_v1::Request::AxisDiscrete {
|
||||||
|
time,
|
||||||
|
axis,
|
||||||
|
value,
|
||||||
|
discrete,
|
||||||
|
} => {
|
||||||
|
let axis = match axis {
|
||||||
|
WEnum::Value(wl_pointer::Axis::VerticalScroll) => Axis::Vertical,
|
||||||
|
WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Axis::Horizontal,
|
||||||
|
_ => {
|
||||||
|
warn!("AxisDiscrete: invalid axis");
|
||||||
|
resource.post_error(
|
||||||
|
zwlr_virtual_pointer_v1::Error::InvalidAxis,
|
||||||
|
"invalid axis",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pointer.mutate_axis_frame(Some(time), |frame| {
|
||||||
|
frame.value(axis, value).v120(axis, discrete)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
zwlr_virtual_pointer_v1::Request::Destroy => {}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroyed(
|
||||||
|
handler: &mut D,
|
||||||
|
_client: wayland_backend::server::ClientId,
|
||||||
|
resource: &ZwlrVirtualPointerV1,
|
||||||
|
_data: &VirtualPointerUserData,
|
||||||
|
) {
|
||||||
|
let pointer = VirtualPointer {
|
||||||
|
pointer: resource.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
handler.destroy_virtual_pointer(pointer);
|
||||||
|
handler
|
||||||
|
.virtual_pointer_manager_state()
|
||||||
|
.virtual_pointers
|
||||||
|
.remove(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! delegate_virtual_pointer {
|
||||||
|
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||||
|
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||||
|
smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1: $crate::protocols::virtual_pointer::VirtualPointerManagerGlobalData
|
||||||
|
] => $crate::protocols::virtual_pointer::VirtualPointerManagerState);
|
||||||
|
|
||||||
|
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||||
|
smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1: ()
|
||||||
|
] => $crate::protocols::virtual_pointer::VirtualPointerManagerState);
|
||||||
|
|
||||||
|
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||||
|
smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1: $crate::protocols::virtual_pointer::VirtualPointerUserData
|
||||||
|
] => $crate::protocols::virtual_pointer::VirtualPointerManagerState);
|
||||||
|
};
|
||||||
|
}
|
||||||
+106
-36
@@ -11,7 +11,7 @@ use anyhow::Context as _;
|
|||||||
use calloop::timer::{TimeoutAction, Timer};
|
use calloop::timer::{TimeoutAction, Timer};
|
||||||
use calloop::RegistrationToken;
|
use calloop::RegistrationToken;
|
||||||
use pipewire::context::Context;
|
use pipewire::context::Context;
|
||||||
use pipewire::core::Core;
|
use pipewire::core::{Core, PW_ID_CORE};
|
||||||
use pipewire::main_loop::MainLoop;
|
use pipewire::main_loop::MainLoop;
|
||||||
use pipewire::properties::Properties;
|
use pipewire::properties::Properties;
|
||||||
use pipewire::spa::buffer::DataType;
|
use pipewire::spa::buffer::DataType;
|
||||||
@@ -36,16 +36,16 @@ use smithay::backend::drm::DrmDeviceFd;
|
|||||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||||
use smithay::backend::renderer::element::RenderElement;
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::output::{Output, OutputModeSource, WeakOutput};
|
use smithay::output::{Output, OutputModeSource};
|
||||||
use smithay::reexports::calloop::generic::Generic;
|
use smithay::reexports::calloop::generic::Generic;
|
||||||
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||||
use smithay::reexports::gbm::Modifier;
|
use smithay::reexports::gbm::Modifier;
|
||||||
use smithay::utils::{Physical, Scale, Size, Transform};
|
use smithay::utils::{Physical, Scale, Size, Transform};
|
||||||
use zbus::SignalContext;
|
use zbus::object_server::SignalEmitter;
|
||||||
|
|
||||||
use crate::dbus::mutter_screen_cast::{self, CursorMode};
|
use crate::dbus::mutter_screen_cast::{self, CursorMode};
|
||||||
use crate::niri::State;
|
use crate::niri::{CastTarget, State};
|
||||||
use crate::render_helpers::render_to_dmabuf;
|
use crate::render_helpers::{clear_dmabuf, render_to_dmabuf};
|
||||||
use crate::utils::get_monotonic_time;
|
use crate::utils::get_monotonic_time;
|
||||||
|
|
||||||
// Give a 0.1 ms allowance for presentation time errors.
|
// Give a 0.1 ms allowance for presentation time errors.
|
||||||
@@ -54,20 +54,24 @@ const CAST_DELAY_ALLOWANCE: Duration = Duration::from_micros(100);
|
|||||||
pub struct PipeWire {
|
pub struct PipeWire {
|
||||||
_context: Context,
|
_context: Context,
|
||||||
pub core: Core,
|
pub core: Core,
|
||||||
|
pub token: RegistrationToken,
|
||||||
to_niri: calloop::channel::Sender<PwToNiri>,
|
to_niri: calloop::channel::Sender<PwToNiri>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum PwToNiri {
|
pub enum PwToNiri {
|
||||||
StopCast { session_id: usize },
|
StopCast { session_id: usize },
|
||||||
Redraw(CastTarget),
|
Redraw { stream_id: usize },
|
||||||
|
FatalError,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Cast {
|
pub struct Cast {
|
||||||
pub session_id: usize,
|
pub session_id: usize,
|
||||||
|
pub stream_id: usize,
|
||||||
pub stream: Stream,
|
pub stream: Stream,
|
||||||
_listener: StreamListener<()>,
|
_listener: StreamListener<()>,
|
||||||
pub is_active: Rc<Cell<bool>>,
|
pub is_active: Rc<Cell<bool>>,
|
||||||
pub target: CastTarget,
|
pub target: CastTarget,
|
||||||
|
pub dynamic_target: bool,
|
||||||
formats: FormatSet,
|
formats: FormatSet,
|
||||||
state: Rc<RefCell<CastState>>,
|
state: Rc<RefCell<CastState>>,
|
||||||
refresh: Rc<Cell<u32>>,
|
refresh: Rc<Cell<u32>>,
|
||||||
@@ -79,6 +83,7 @@ pub struct Cast {
|
|||||||
scheduled_redraw: Option<RegistrationToken>,
|
scheduled_redraw: Option<RegistrationToken>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum CastState {
|
pub enum CastState {
|
||||||
ResizePending {
|
ResizePending {
|
||||||
@@ -106,12 +111,6 @@ pub enum CastSizeChange {
|
|||||||
Pending,
|
Pending,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
|
||||||
pub enum CastTarget {
|
|
||||||
Output(WeakOutput),
|
|
||||||
Window { id: u64 },
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! make_params {
|
macro_rules! make_params {
|
||||||
($params:ident, $formats:expr, $size:expr, $refresh:expr, $alpha:expr) => {
|
($params:ident, $formats:expr, $size:expr, $refresh:expr, $alpha:expr) => {
|
||||||
let mut b1 = Vec::new();
|
let mut b1 = Vec::new();
|
||||||
@@ -134,15 +133,26 @@ macro_rules! make_params {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PipeWire {
|
impl PipeWire {
|
||||||
pub fn new(event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
|
pub fn new(
|
||||||
|
event_loop: &LoopHandle<'static, State>,
|
||||||
|
to_niri: calloop::channel::Sender<PwToNiri>,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
let main_loop = MainLoop::new(None).context("error creating MainLoop")?;
|
let main_loop = MainLoop::new(None).context("error creating MainLoop")?;
|
||||||
let context = Context::new(&main_loop).context("error creating Context")?;
|
let context = Context::new(&main_loop).context("error creating Context")?;
|
||||||
let core = context.connect(None).context("error creating Core")?;
|
let core = context.connect(None).context("error creating Core")?;
|
||||||
|
|
||||||
|
let to_niri_ = to_niri.clone();
|
||||||
let listener = core
|
let listener = core
|
||||||
.add_listener_local()
|
.add_listener_local()
|
||||||
.error(|id, seq, res, message| {
|
.error(move |id, seq, res, message| {
|
||||||
warn!(id, seq, res, message, "pw error");
|
warn!(id, seq, res, message, "pw error");
|
||||||
|
|
||||||
|
// Reset PipeWire on connection errors.
|
||||||
|
if id == PW_ID_CORE && res == -32 {
|
||||||
|
if let Err(err) = to_niri_.send(PwToNiri::FatalError) {
|
||||||
|
warn!("error sending FatalError to niri: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.register();
|
.register();
|
||||||
mem::forget(listener);
|
mem::forget(listener);
|
||||||
@@ -154,7 +164,7 @@ impl PipeWire {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let generic = Generic::new(AsFdWrapper(main_loop), Interest::READ, Mode::Level);
|
let generic = Generic::new(AsFdWrapper(main_loop), Interest::READ, Mode::Level);
|
||||||
event_loop
|
let token = event_loop
|
||||||
.insert_source(generic, move |_, wrapper, _| {
|
.insert_source(generic, move |_, wrapper, _| {
|
||||||
let _span = tracy_client::span!("pipewire iteration");
|
let _span = tracy_client::span!("pipewire iteration");
|
||||||
wrapper.0.loop_().iterate(Duration::ZERO);
|
wrapper.0.loop_().iterate(Duration::ZERO);
|
||||||
@@ -162,17 +172,10 @@ impl PipeWire {
|
|||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (to_niri, from_pipewire) = calloop::channel::channel();
|
|
||||||
event_loop
|
|
||||||
.insert_source(from_pipewire, move |event, _, state| match event {
|
|
||||||
calloop::channel::Event::Msg(msg) => state.on_pw_msg(msg),
|
|
||||||
calloop::channel::Event::Closed => (),
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
_context: context,
|
_context: context,
|
||||||
core,
|
core,
|
||||||
|
token,
|
||||||
to_niri,
|
to_niri,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -183,12 +186,14 @@ impl PipeWire {
|
|||||||
gbm: GbmDevice<DrmDeviceFd>,
|
gbm: GbmDevice<DrmDeviceFd>,
|
||||||
formats: FormatSet,
|
formats: FormatSet,
|
||||||
session_id: usize,
|
session_id: usize,
|
||||||
|
stream_id: usize,
|
||||||
target: CastTarget,
|
target: CastTarget,
|
||||||
|
dynamic_target: bool,
|
||||||
size: Size<i32, Physical>,
|
size: Size<i32, Physical>,
|
||||||
refresh: u32,
|
refresh: u32,
|
||||||
alpha: bool,
|
alpha: bool,
|
||||||
cursor_mode: CursorMode,
|
cursor_mode: CursorMode,
|
||||||
signal_ctx: SignalContext<'static>,
|
signal_ctx: SignalEmitter<'static>,
|
||||||
) -> anyhow::Result<Cast> {
|
) -> anyhow::Result<Cast> {
|
||||||
let _span = tracy_client::span!("PipeWire::start_cast");
|
let _span = tracy_client::span!("PipeWire::start_cast");
|
||||||
|
|
||||||
@@ -198,10 +203,9 @@ impl PipeWire {
|
|||||||
warn!("error sending StopCast to niri: {err:?}");
|
warn!("error sending StopCast to niri: {err:?}");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let target_ = target.clone();
|
|
||||||
let to_niri_ = self.to_niri.clone();
|
let to_niri_ = self.to_niri.clone();
|
||||||
let redraw = move || {
|
let redraw = move || {
|
||||||
if let Err(err) = to_niri_.send(PwToNiri::Redraw(target_.clone())) {
|
if let Err(err) = to_niri_.send(PwToNiri::Redraw { stream_id }) {
|
||||||
warn!("error sending Redraw to niri: {err:?}");
|
warn!("error sending Redraw to niri: {err:?}");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -645,10 +649,12 @@ impl PipeWire {
|
|||||||
|
|
||||||
let cast = Cast {
|
let cast = Cast {
|
||||||
session_id,
|
session_id,
|
||||||
|
stream_id,
|
||||||
stream,
|
stream,
|
||||||
_listener: listener,
|
_listener: listener,
|
||||||
is_active,
|
is_active,
|
||||||
target,
|
target,
|
||||||
|
dynamic_target,
|
||||||
formats,
|
formats,
|
||||||
state,
|
state,
|
||||||
refresh,
|
refresh,
|
||||||
@@ -769,7 +775,11 @@ impl Cast {
|
|||||||
let timer = Timer::from_duration(duration);
|
let timer = Timer::from_duration(duration);
|
||||||
let token = event_loop
|
let token = event_loop
|
||||||
.insert_source(timer, move |_, _, state| {
|
.insert_source(timer, move |_, _, state| {
|
||||||
state.niri.queue_redraw(&output);
|
// Guard against output disconnecting before the timer has a chance to run.
|
||||||
|
if state.niri.output_state.contains_key(&output) {
|
||||||
|
state.niri.queue_redraw(&output);
|
||||||
|
}
|
||||||
|
|
||||||
TimeoutAction::Drop
|
TimeoutAction::Drop
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -812,6 +822,7 @@ impl Cast {
|
|||||||
elements: &[impl RenderElement<GlesRenderer>],
|
elements: &[impl RenderElement<GlesRenderer>],
|
||||||
size: Size<i32, Physical>,
|
size: Size<i32, Physical>,
|
||||||
scale: Scale<f64>,
|
scale: Scale<f64>,
|
||||||
|
wait_for_sync: bool,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let CastState::Ready { damage_tracker, .. } = &mut *self.state.borrow_mut() else {
|
let CastState::Ready { damage_tracker, .. } = &mut *self.state.borrow_mut() else {
|
||||||
error!("cast must be in Ready state to render");
|
error!("cast must be in Ready state to render");
|
||||||
@@ -834,18 +845,15 @@ impl Cast {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut buffer = match self.stream.dequeue_buffer() {
|
let Some(mut buffer) = self.stream.dequeue_buffer() else {
|
||||||
Some(buffer) => buffer,
|
warn!("no available buffer in pw stream, skipping frame");
|
||||||
None => {
|
return false;
|
||||||
warn!("no available buffer in pw stream, skipping frame");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let fd = buffer.datas_mut()[0].as_raw().fd;
|
let fd = buffer.datas_mut()[0].as_raw().fd;
|
||||||
let dmabuf = &self.dmabufs.borrow()[&fd];
|
let dmabuf = &self.dmabufs.borrow()[&fd];
|
||||||
|
|
||||||
if let Err(err) = render_to_dmabuf(
|
match render_to_dmabuf(
|
||||||
renderer,
|
renderer,
|
||||||
dmabuf.clone(),
|
dmabuf.clone(),
|
||||||
size,
|
size,
|
||||||
@@ -853,8 +861,70 @@ impl Cast {
|
|||||||
Transform::Normal,
|
Transform::Normal,
|
||||||
elements.iter().rev(),
|
elements.iter().rev(),
|
||||||
) {
|
) {
|
||||||
warn!("error rendering to dmabuf: {err:?}");
|
Ok(sync_point) => {
|
||||||
|
// FIXME: implement PipeWire explicit sync, and at the very least async wait.
|
||||||
|
if wait_for_sync {
|
||||||
|
let _span = tracy_client::span!("wait for completion");
|
||||||
|
if let Err(err) = sync_point.wait() {
|
||||||
|
warn!("error waiting for pw frame completion: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error rendering to dmabuf: {err:?}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (data, (stride, offset)) in
|
||||||
|
zip(buffer.datas_mut(), zip(dmabuf.strides(), dmabuf.offsets()))
|
||||||
|
{
|
||||||
|
let chunk = data.chunk_mut();
|
||||||
|
*chunk.size_mut() = 1;
|
||||||
|
*chunk.stride_mut() = stride as i32;
|
||||||
|
*chunk.offset_mut() = offset;
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"pw buffer: fd = {}, stride = {stride}, offset = {offset}",
|
||||||
|
data.as_raw().fd
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dequeue_buffer_and_clear(
|
||||||
|
&mut self,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
wait_for_sync: bool,
|
||||||
|
) -> bool {
|
||||||
|
// Clear out the damage tracker if we're in Ready state.
|
||||||
|
if let CastState::Ready { damage_tracker, .. } = &mut *self.state.borrow_mut() {
|
||||||
|
*damage_tracker = None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(mut buffer) = self.stream.dequeue_buffer() else {
|
||||||
|
warn!("no available buffer in pw stream, skipping clear");
|
||||||
return false;
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let fd = buffer.datas_mut()[0].as_raw().fd;
|
||||||
|
let dmabuf = &self.dmabufs.borrow()[&fd];
|
||||||
|
|
||||||
|
match clear_dmabuf(renderer, dmabuf.clone()) {
|
||||||
|
Ok(sync_point) => {
|
||||||
|
// FIXME: implement PipeWire explicit sync, and at the very least async wait.
|
||||||
|
if wait_for_sync {
|
||||||
|
let _span = tracy_client::span!("wait for completion");
|
||||||
|
if let Err(err) = sync_point.wait() {
|
||||||
|
warn!("error waiting for pw frame completion: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error clearing dmabuf: {err:?}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (data, (stride, offset)) in
|
for (data, (stride, offset)) in
|
||||||
@@ -1023,7 +1093,7 @@ fn allocate_buffer(
|
|||||||
.create_buffer_object_with_modifiers2::<()>(w, h, fourcc, modifiers, flags)
|
.create_buffer_object_with_modifiers2::<()>(w, h, fourcc, modifiers, flags)
|
||||||
.context("error creating GBM buffer object")?;
|
.context("error creating GBM buffer object")?;
|
||||||
|
|
||||||
let modifier = bo.modifier().unwrap();
|
let modifier = bo.modifier();
|
||||||
let buffer = GbmBuffer::from_bo(bo, false);
|
let buffer = GbmBuffer::from_bo(bo, false);
|
||||||
Ok((buffer, modifier))
|
Ok((buffer, modifier))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ struct Parameters {
|
|||||||
corner_radius: CornerRadius,
|
corner_radius: CornerRadius,
|
||||||
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
|
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
|
||||||
scale: f32,
|
scale: f32,
|
||||||
|
alpha: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BorderRenderElement {
|
impl BorderRenderElement {
|
||||||
@@ -54,6 +55,7 @@ impl BorderRenderElement {
|
|||||||
border_width: f32,
|
border_width: f32,
|
||||||
corner_radius: CornerRadius,
|
corner_radius: CornerRadius,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
|
alpha: f32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let inner = ShaderRenderElement::empty(ProgramType::Border, Kind::Unspecified);
|
let inner = ShaderRenderElement::empty(ProgramType::Border, Kind::Unspecified);
|
||||||
let mut rv = Self {
|
let mut rv = Self {
|
||||||
@@ -69,6 +71,7 @@ impl BorderRenderElement {
|
|||||||
border_width,
|
border_width,
|
||||||
corner_radius,
|
corner_radius,
|
||||||
scale,
|
scale,
|
||||||
|
alpha,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
rv.update_inner();
|
rv.update_inner();
|
||||||
@@ -90,6 +93,7 @@ impl BorderRenderElement {
|
|||||||
border_width: 0.,
|
border_width: 0.,
|
||||||
corner_radius: Default::default(),
|
corner_radius: Default::default(),
|
||||||
scale: 1.,
|
scale: 1.,
|
||||||
|
alpha: 1.,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,6 +115,7 @@ impl BorderRenderElement {
|
|||||||
border_width: f32,
|
border_width: f32,
|
||||||
corner_radius: CornerRadius,
|
corner_radius: CornerRadius,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
|
alpha: f32,
|
||||||
) {
|
) {
|
||||||
let params = Parameters {
|
let params = Parameters {
|
||||||
size,
|
size,
|
||||||
@@ -123,6 +128,7 @@ impl BorderRenderElement {
|
|||||||
border_width,
|
border_width,
|
||||||
corner_radius,
|
corner_radius,
|
||||||
scale,
|
scale,
|
||||||
|
alpha,
|
||||||
};
|
};
|
||||||
if self.params == params {
|
if self.params == params {
|
||||||
return;
|
return;
|
||||||
@@ -144,6 +150,7 @@ impl BorderRenderElement {
|
|||||||
border_width,
|
border_width,
|
||||||
corner_radius,
|
corner_radius,
|
||||||
scale,
|
scale,
|
||||||
|
alpha,
|
||||||
} = self.params;
|
} = self.params;
|
||||||
|
|
||||||
let grad_offset = geometry.loc - gradient_area.loc;
|
let grad_offset = geometry.loc - gradient_area.loc;
|
||||||
@@ -189,6 +196,7 @@ impl BorderRenderElement {
|
|||||||
size,
|
size,
|
||||||
None,
|
None,
|
||||||
scale,
|
scale,
|
||||||
|
alpha,
|
||||||
vec![
|
vec![
|
||||||
Uniform::new("colorspace", colorspace),
|
Uniform::new("colorspace", colorspace),
|
||||||
Uniform::new("hue_interpolation", hue_interpolation),
|
Uniform::new("hue_interpolation", hue_interpolation),
|
||||||
@@ -269,7 +277,7 @@ impl Element for BorderRenderElement {
|
|||||||
impl RenderElement<GlesRenderer> for BorderRenderElement {
|
impl RenderElement<GlesRenderer> for BorderRenderElement {
|
||||||
fn draw(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
frame: &mut GlesFrame<'_>,
|
frame: &mut GlesFrame<'_, '_>,
|
||||||
src: Rectangle<f64, Buffer>,
|
src: Rectangle<f64, Buffer>,
|
||||||
dst: Rectangle<i32, Physical>,
|
dst: Rectangle<i32, Physical>,
|
||||||
damage: &[Rectangle<i32, Physical>],
|
damage: &[Rectangle<i32, Physical>],
|
||||||
@@ -286,7 +294,7 @@ impl RenderElement<GlesRenderer> for BorderRenderElement {
|
|||||||
impl<'render> RenderElement<TtyRenderer<'render>> for BorderRenderElement {
|
impl<'render> RenderElement<TtyRenderer<'render>> for BorderRenderElement {
|
||||||
fn draw(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
frame: &mut TtyFrame<'_, '_>,
|
frame: &mut TtyFrame<'_, '_, '_>,
|
||||||
src: Rectangle<f64, Buffer>,
|
src: Rectangle<f64, Buffer>,
|
||||||
dst: Rectangle<i32, Physical>,
|
dst: Rectangle<i32, Physical>,
|
||||||
damage: &[Rectangle<i32, Physical>],
|
damage: &[Rectangle<i32, Physical>],
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use smithay::backend::renderer::gles::{
|
|||||||
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, Uniform,
|
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, Uniform,
|
||||||
};
|
};
|
||||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
|
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
|
||||||
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform};
|
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||||
|
|
||||||
use super::damage::ExtraDamage;
|
use super::damage::ExtraDamage;
|
||||||
use super::renderer::{AsGlesFrame as _, NiriRenderer};
|
use super::renderer::{AsGlesFrame as _, NiriRenderer};
|
||||||
@@ -19,9 +19,7 @@ pub struct ClippedSurfaceRenderElement<R: NiriRenderer> {
|
|||||||
program: GlesTexProgram,
|
program: GlesTexProgram,
|
||||||
corner_radius: CornerRadius,
|
corner_radius: CornerRadius,
|
||||||
geometry: Rectangle<f64, Logical>,
|
geometry: Rectangle<f64, Logical>,
|
||||||
input_to_geo: Mat3,
|
uniforms: Vec<Uniform<'static>>,
|
||||||
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
|
|
||||||
scale: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
@@ -72,13 +70,19 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
|
|||||||
* Mat3::from_scale(buf_size / src_size)
|
* Mat3::from_scale(buf_size / src_size)
|
||||||
* Mat3::from_translation(-src_loc / buf_size);
|
* Mat3::from_translation(-src_loc / buf_size);
|
||||||
|
|
||||||
|
let uniforms = vec![
|
||||||
|
Uniform::new("niri_scale", scale.x as f32),
|
||||||
|
Uniform::new("geo_size", (geometry.size.w as f32, geometry.size.h as f32)),
|
||||||
|
Uniform::new("corner_radius", <[f32; 4]>::from(corner_radius)),
|
||||||
|
mat3_uniform("input_to_geo", input_to_geo),
|
||||||
|
];
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
inner: elem,
|
inner: elem,
|
||||||
program,
|
program,
|
||||||
corner_radius,
|
corner_radius,
|
||||||
geometry,
|
geometry,
|
||||||
input_to_geo,
|
uniforms,
|
||||||
scale: scale.x as f32,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,21 +121,21 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
|
|||||||
let bottom_left = corner_radius.bottom_left as f64;
|
let bottom_left = corner_radius.bottom_left as f64;
|
||||||
|
|
||||||
[
|
[
|
||||||
Rectangle::from_loc_and_size(geo.loc, (top_left, top_left)),
|
Rectangle::new(geo.loc, Size::from((top_left, top_left))),
|
||||||
Rectangle::from_loc_and_size(
|
Rectangle::new(
|
||||||
(geo.loc.x + geo.size.w - top_right, geo.loc.y),
|
Point::from((geo.loc.x + geo.size.w - top_right, geo.loc.y)),
|
||||||
(top_right, top_right),
|
Size::from((top_right, top_right)),
|
||||||
),
|
),
|
||||||
Rectangle::from_loc_and_size(
|
Rectangle::new(
|
||||||
(
|
Point::from((
|
||||||
geo.loc.x + geo.size.w - bottom_right,
|
geo.loc.x + geo.size.w - bottom_right,
|
||||||
geo.loc.y + geo.size.h - bottom_right,
|
geo.loc.y + geo.size.h - bottom_right,
|
||||||
),
|
)),
|
||||||
(bottom_right, bottom_right),
|
Size::from((bottom_right, bottom_right)),
|
||||||
),
|
),
|
||||||
Rectangle::from_loc_and_size(
|
Rectangle::new(
|
||||||
(geo.loc.x, geo.loc.y + geo.size.h - bottom_left),
|
Point::from((geo.loc.x, geo.loc.y + geo.size.h - bottom_left)),
|
||||||
(bottom_left, bottom_left),
|
Size::from((bottom_left, bottom_left)),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -214,24 +218,13 @@ impl<R: NiriRenderer> Element for ClippedSurfaceRenderElement<R> {
|
|||||||
impl RenderElement<GlesRenderer> for ClippedSurfaceRenderElement<GlesRenderer> {
|
impl RenderElement<GlesRenderer> for ClippedSurfaceRenderElement<GlesRenderer> {
|
||||||
fn draw(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
frame: &mut GlesFrame<'_>,
|
frame: &mut GlesFrame<'_, '_>,
|
||||||
src: Rectangle<f64, Buffer>,
|
src: Rectangle<f64, Buffer>,
|
||||||
dst: Rectangle<i32, Physical>,
|
dst: Rectangle<i32, Physical>,
|
||||||
damage: &[Rectangle<i32, Physical>],
|
damage: &[Rectangle<i32, Physical>],
|
||||||
opaque_regions: &[Rectangle<i32, Physical>],
|
opaque_regions: &[Rectangle<i32, Physical>],
|
||||||
) -> Result<(), GlesError> {
|
) -> Result<(), GlesError> {
|
||||||
frame.override_default_tex_program(
|
frame.override_default_tex_program(self.program.clone(), self.uniforms.clone());
|
||||||
self.program.clone(),
|
|
||||||
vec![
|
|
||||||
Uniform::new("niri_scale", self.scale),
|
|
||||||
Uniform::new(
|
|
||||||
"geo_size",
|
|
||||||
(self.geometry.size.w as f32, self.geometry.size.h as f32),
|
|
||||||
),
|
|
||||||
Uniform::new("corner_radius", <[f32; 4]>::from(self.corner_radius)),
|
|
||||||
mat3_uniform("input_to_geo", self.input_to_geo),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
|
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
|
||||||
frame.clear_tex_program_override();
|
frame.clear_tex_program_override();
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -249,23 +242,15 @@ impl<'render> RenderElement<TtyRenderer<'render>>
|
|||||||
{
|
{
|
||||||
fn draw(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
frame: &mut TtyFrame<'render, '_>,
|
frame: &mut TtyFrame<'render, '_, '_>,
|
||||||
src: Rectangle<f64, Buffer>,
|
src: Rectangle<f64, Buffer>,
|
||||||
dst: Rectangle<i32, Physical>,
|
dst: Rectangle<i32, Physical>,
|
||||||
damage: &[Rectangle<i32, Physical>],
|
damage: &[Rectangle<i32, Physical>],
|
||||||
opaque_regions: &[Rectangle<i32, Physical>],
|
opaque_regions: &[Rectangle<i32, Physical>],
|
||||||
) -> Result<(), TtyRendererError<'render>> {
|
) -> Result<(), TtyRendererError<'render>> {
|
||||||
frame.as_gles_frame().override_default_tex_program(
|
frame
|
||||||
self.program.clone(),
|
.as_gles_frame()
|
||||||
vec![
|
.override_default_tex_program(self.program.clone(), self.uniforms.clone());
|
||||||
Uniform::new(
|
|
||||||
"geo_size",
|
|
||||||
(self.geometry.size.w as f32, self.geometry.size.h as f32),
|
|
||||||
),
|
|
||||||
Uniform::new("corner_radius", <[f32; 4]>::from(self.corner_radius)),
|
|
||||||
mat3_uniform("input_to_geo", self.input_to_geo),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
RenderElement::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
|
RenderElement::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
|
||||||
frame.as_gles_frame().clear_tex_program_override();
|
frame.as_gles_frame().clear_tex_program_override();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user