Compare commits
689 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df910786a6 | ||
|
|
d902343bb4 | ||
|
|
ecb85dd5bf | ||
|
|
e4ab87bc2a | ||
|
|
37b57aee60 | ||
|
|
33ef1720e3 | ||
|
|
c41a495ee0 | ||
|
|
47f93e9cab | ||
|
|
9e91ae61a5 | ||
|
|
87cc157055 | ||
|
|
29983ab9da | ||
|
|
ff9db50831 | ||
|
|
4cb6ac05d3 | ||
|
|
73a64944b8 | ||
|
|
d25c2db420 | ||
|
|
216775f55a | ||
|
|
ad79e81c50 | ||
|
|
df7226d06c | ||
|
|
6e03473133 | ||
|
|
36c58e211d | ||
|
|
2335ca5fe5 | ||
|
|
8865fc0f6f | ||
|
|
f6eec1fe51 | ||
|
|
70de347bcf | ||
|
|
2cbf64e250 | ||
|
|
a14e559c80 | ||
|
|
59fb1b85eb | ||
|
|
4197a863e5 | ||
|
|
4293c6877d | ||
|
|
f393627932 | ||
|
|
c54ce27505 | ||
|
|
97518eb49c | ||
|
|
ddce15481f | ||
|
|
43721426e8 | ||
|
|
6baa9c7858 | ||
|
|
8ec080a290 | ||
|
|
166c440978 | ||
|
|
6e9dca9d42 | ||
|
|
b50f41b1e0 | ||
|
|
357dcaacbc | ||
|
|
61e681e6bd | ||
|
|
ebd4264621 | ||
|
|
4167c64fde | ||
|
|
6456b2998d | ||
|
|
4de5ab3c59 | ||
|
|
04ca79a5af | ||
|
|
0d803fd962 | ||
|
|
8406c86117 | ||
|
|
1aee3163e0 | ||
|
|
9aec974e9e | ||
|
|
4de148be58 | ||
|
|
3f48178333 | ||
|
|
8af01e44f2 | ||
|
|
a76440f242 | ||
|
|
49c0d14ace | ||
|
|
6a6856192c | ||
|
|
b821d8bb6a | ||
|
|
e12d5f7fb9 | ||
|
|
26ee4e5560 | ||
|
|
622a9c1b06 | ||
|
|
6b0e94698b | ||
|
|
08e1f2d5f3 | ||
|
|
b53431d861 | ||
|
|
327b223998 | ||
|
|
e26cd16ef9 | ||
|
|
669e7e9566 | ||
|
|
9bb8a9e3dd | ||
|
|
249cacdd0a | ||
|
|
167fe26f30 | ||
|
|
44a86e0937 | ||
|
|
f5e1b61dcf | ||
|
|
1ca23373e1 | ||
|
|
78dafcc5b3 | ||
|
|
e92ccab3d1 | ||
|
|
8820a0f274 | ||
|
|
9c15f8aed1 | ||
|
|
f5ad63ed33 | ||
|
|
7abc39d051 | ||
|
|
47745fa501 | ||
|
|
4c2e460127 | ||
|
|
fa567b06d0 | ||
|
|
892c09512c | ||
|
|
8be2f01058 | ||
|
|
673ef2e09d | ||
|
|
cd271ed283 | ||
|
|
98e370ac60 | ||
|
|
425ef5020b | ||
|
|
91e7a86e7a | ||
|
|
b369f648db | ||
|
|
bfe1d248a4 | ||
|
|
902ca67c58 | ||
|
|
2ac527bcbc | ||
|
|
60af15fb8d | ||
|
|
b3043afe04 | ||
|
|
9458d95222 | ||
|
|
8f15274fb9 | ||
|
|
19b790d4fa | ||
|
|
74718b064f | ||
|
|
7095cb6637 | ||
|
|
7dc64014fd | ||
|
|
66bc9f6d74 | ||
|
|
4b3240b9b9 | ||
|
|
3803141f31 | ||
|
|
2ed91786a3 | ||
|
|
5e827a73c8 | ||
|
|
58957ec597 | ||
|
|
abafe1b6fd | ||
|
|
6536035e19 | ||
|
|
187a367977 | ||
|
|
e0ae5c97d4 | ||
|
|
59b3cdd0dc | ||
|
|
5cf8659f92 | ||
|
|
0b14629dd7 | ||
|
|
726093f534 | ||
|
|
39a389b90c | ||
|
|
140643d4ec | ||
|
|
f52164da87 | ||
|
|
926c5bae7e | ||
|
|
ac8375bf47 | ||
|
|
58667c77ed | ||
|
|
b06c3e0e3f | ||
|
|
21d50ed7e6 | ||
|
|
5f576315fe | ||
|
|
da57afb395 | ||
|
|
c0ef865920 | ||
|
|
3afc8fa9ca | ||
|
|
7a4191df95 | ||
|
|
18832c2956 | ||
|
|
4974df29d6 | ||
|
|
b72700fbd2 | ||
|
|
361fe49001 | ||
|
|
db5e2055e1 | ||
|
|
3ef6861b62 | ||
|
|
3156b2b85a | ||
|
|
d2f8d3dfe3 | ||
|
|
fc6f154021 | ||
|
|
f3b2f4bce7 | ||
|
|
edad7fb3b7 | ||
|
|
3631b67496 | ||
|
|
b118409e83 | ||
|
|
34706b1ae8 | ||
|
|
8927256f39 | ||
|
|
6d667538db | ||
|
|
54145617b5 | ||
|
|
49057eec5d | ||
|
|
b144571c9a | ||
|
|
30a4b63f00 | ||
|
|
0e2307ea03 | ||
|
|
dcdcc2dbc5 | ||
|
|
9a37ab6e7b | ||
|
|
b398ea4749 | ||
|
|
c29cc74de0 | ||
|
|
f571c406c7 | ||
|
|
517b31d62a | ||
|
|
a6953310b1 | ||
|
|
46591b1b8a | ||
|
|
67c789ae41 | ||
|
|
5ce4cedd3e | ||
|
|
df5c4a0678 | ||
|
|
29176aacf0 | ||
|
|
345a57c465 | ||
|
|
bf1e1622ac | ||
|
|
b9ce839f83 | ||
|
|
8d025d1958 | ||
|
|
732acbdf5c | ||
|
|
975ebc7ee5 | ||
|
|
03baaf3099 | ||
|
|
7ba9d95f96 | ||
|
|
3d08b3e873 | ||
|
|
fa31aa9299 | ||
|
|
7f72d0805e | ||
|
|
ac5432c93c | ||
|
|
19db669204 | ||
|
|
e13bb1f551 | ||
|
|
8fbad0c9be | ||
|
|
994ee6de29 | ||
|
|
0bd437603b | ||
|
|
1891165d50 | ||
|
|
45657e145c | ||
|
|
ca63ebe504 | ||
|
|
66951f8fba | ||
|
|
609e023f9b | ||
|
|
294ae868a6 | ||
|
|
0e15ffa23a | ||
|
|
5f62a00c60 | ||
|
|
bf1b24bb95 | ||
|
|
27dacde798 | ||
|
|
9004f186fa | ||
|
|
b9e6e8ba0c | ||
|
|
ed15ddfaaa | ||
|
|
881910f1b3 | ||
|
|
7936e4f881 | ||
|
|
9072ff8525 | ||
|
|
1c85310c22 | ||
|
|
b65c1b5c23 | ||
|
|
2840138464 | ||
|
|
205fe2f9bf | ||
|
|
6a7e77029d | ||
|
|
f36cb58237 | ||
|
|
c14f2b4fc6 | ||
|
|
243408d176 | ||
|
|
b647eb36af | ||
|
|
4238b5fe9a | ||
|
|
3120d7556e | ||
|
|
a9ccb25cae | ||
|
|
6d32f4f823 | ||
|
|
22f9e7e272 | ||
|
|
e73173888c | ||
|
|
faf8afdd65 | ||
|
|
f2fc0ca891 | ||
|
|
e7346cb28a | ||
|
|
bbc99df84d | ||
|
|
cf0367e069 | ||
|
|
1f1b1b2b01 | ||
|
|
a6841935cc | ||
|
|
96039a47aa | ||
|
|
2d30974f85 | ||
|
|
c5949be319 | ||
|
|
f21833b641 | ||
|
|
35cb12b2f2 | ||
|
|
4efd34f78b | ||
|
|
d35161d5d1 | ||
|
|
fd73225688 | ||
|
|
b7b7f15420 | ||
|
|
b910eb741a | ||
|
|
fec83417f5 | ||
|
|
b3f2c3f8a9 | ||
|
|
1eaf773206 | ||
|
|
38222328c2 | ||
|
|
dc04096587 | ||
|
|
52182d3b4e | ||
|
|
c59f3f3683 | ||
|
|
348b4bcf98 | ||
|
|
f811704e5e | ||
|
|
c2867b6fc8 | ||
|
|
972a5bb357 | ||
|
|
eb73c5b08f | ||
|
|
53607bf019 | ||
|
|
21216b642c | ||
|
|
39c284682a | ||
|
|
5d367195ed | ||
|
|
b38ed83289 | ||
|
|
22ae76c2ce | ||
|
|
1ff85fef2f | ||
|
|
160ec69391 | ||
|
|
a97e5fb762 | ||
|
|
98214786d8 | ||
|
|
01e6f2d6b8 | ||
|
|
9a92276bd0 | ||
|
|
d491a76c43 | ||
|
|
9592f9333a | ||
|
|
fda4b68c56 | ||
|
|
317b6162ea | ||
|
|
5dc6f3be30 | ||
|
|
a999cc8948 | ||
|
|
ef700d9d85 | ||
|
|
bd6e540d04 | ||
|
|
68e0f9e35a | ||
|
|
9e3fcf2e09 | ||
|
|
34019e65ba | ||
|
|
df3fb81b6a | ||
|
|
4fabb49ed3 | ||
|
|
7db5a1305c | ||
|
|
14b5cff081 | ||
|
|
89bb47e53e | ||
|
|
14b54e7790 | ||
|
|
b0044d6f0a | ||
|
|
c0f1a2e1c8 | ||
|
|
50d95f1870 | ||
|
|
e4187a6d4c | ||
|
|
b5a639936b | ||
|
|
4ca6716074 | ||
|
|
1937508781 | ||
|
|
37be5104b2 | ||
|
|
976f0b4e34 | ||
|
|
ab1b2a8a96 | ||
|
|
2081d13b81 | ||
|
|
164b7858f7 | ||
|
|
fb528adeb9 | ||
|
|
4aecfa43ab | ||
|
|
6bc8822e04 | ||
|
|
095e99bc0a | ||
|
|
623a79da54 | ||
|
|
9e9d57be8e | ||
|
|
cbe77b3b1f | ||
|
|
aff9a999b0 | ||
|
|
588f843e0c | ||
|
|
0957ef2804 | ||
|
|
c571bfb03b | ||
|
|
d7445dabb2 | ||
|
|
1dac50f70a | ||
|
|
3a04893caa | ||
|
|
83c292de6b | ||
|
|
a2e18ee7df | ||
|
|
84c455f557 | ||
|
|
22981292cc | ||
|
|
4d0d84c44d | ||
|
|
b9d5fd8e68 | ||
|
|
826d560b84 | ||
|
|
5b47b67c83 | ||
|
|
0561ecba3f | ||
|
|
c9631268bd | ||
|
|
11510bd8b4 | ||
|
|
a58694a04d | ||
|
|
9667a73b05 | ||
|
|
24b57fb269 | ||
|
|
cf1dc459a2 | ||
|
|
306c4c542b | ||
|
|
61bb98460d | ||
|
|
53116df6a0 | ||
|
|
29c70d26c7 | ||
|
|
ee40a2265a | ||
|
|
167df98182 | ||
|
|
904ceb8f09 | ||
|
|
307ba2fe61 | ||
|
|
87b615999d | ||
|
|
1e8ab7f983 | ||
|
|
2293993865 | ||
|
|
69bf08729d | ||
|
|
4fa04a4b6e | ||
|
|
0572480239 | ||
|
|
092ecb407d | ||
|
|
e63886b402 | ||
|
|
4b18b6c4f1 | ||
|
|
678c17c80f | ||
|
|
076cba0c85 | ||
|
|
f9beb589db | ||
|
|
3f439a2aa0 | ||
|
|
b8927722aa | ||
|
|
a0945ecc84 | ||
|
|
e51bdc3647 | ||
|
|
a6b87183e1 | ||
|
|
a491077fca | ||
|
|
2a369d0532 | ||
|
|
e83239af24 | ||
|
|
34a4664499 | ||
|
|
1606c61745 | ||
|
|
ade3c45be5 | ||
|
|
7efda1ba42 | ||
|
|
25c9890fe3 | ||
|
|
baf8c71909 | ||
|
|
369d75af26 | ||
|
|
cf66eb8853 | ||
|
|
f714c9211d | ||
|
|
2d34db53fe | ||
|
|
a7f0065e85 | ||
|
|
8fdb800dea | ||
|
|
a747c5baba | ||
|
|
b4238e06b3 | ||
|
|
2d2e288f5c | ||
|
|
1326716b0b | ||
|
|
a6254b4308 | ||
|
|
f1e2e9561c | ||
|
|
1d5f3bbecd | ||
|
|
a435cb8a20 | ||
|
|
d3d0c59a80 | ||
|
|
f947c97b83 | ||
|
|
5d53c08fd7 | ||
|
|
cb52e7047c | ||
|
|
23a7d22062 | ||
|
|
af7cf437a3 | ||
|
|
adfa2d9c58 | ||
|
|
62d6092956 | ||
|
|
84bd33bfd6 | ||
|
|
9b13f4cea7 | ||
|
|
e2614a9832 | ||
|
|
d1ecb16823 | ||
|
|
e21d8fd836 | ||
|
|
2ffac9e6ca | ||
|
|
9de5181aaa | ||
|
|
e35477aecc | ||
|
|
9930241970 | ||
|
|
494a8da3f7 | ||
|
|
aa25c4735e | ||
|
|
1d260f65bc | ||
|
|
3c6cbe1638 | ||
|
|
fa63a99ae3 | ||
|
|
da1c22f525 | ||
|
|
70bbb630cc | ||
|
|
44814071c8 | ||
|
|
d3a8f22bfb | ||
|
|
43b31cb814 | ||
|
|
40d5972533 | ||
|
|
26329aa75f | ||
|
|
4b698f48fa | ||
|
|
786ebf04d4 | ||
|
|
9fbfcca1c2 | ||
|
|
daf11c8d1a | ||
|
|
28809ff783 | ||
|
|
48dbdeb5c6 | ||
|
|
09bba44229 | ||
|
|
6e8207cb56 | ||
|
|
557254f74a | ||
|
|
3a72ddf3d7 | ||
|
|
043892d23b | ||
|
|
0490604277 | ||
|
|
584a6e5911 | ||
|
|
049a0337b2 | ||
|
|
a42cc5df37 | ||
|
|
4d29933271 | ||
|
|
00ba7af473 | ||
|
|
f2a177bc79 | ||
|
|
04aff76ea4 | ||
|
|
6ff1c07c42 | ||
|
|
c23d0d29dd | ||
|
|
e8d08f8c43 | ||
|
|
9b6f310d4f | ||
|
|
f281396b6f | ||
|
|
95ebf251a2 | ||
|
|
a14163bfca | ||
|
|
c64d4e5cb5 | ||
|
|
9bd70df8bf | ||
|
|
8f68cc6abc | ||
|
|
59709c3ddf | ||
|
|
405e50effb | ||
|
|
631809af3c | ||
|
|
90f95c2963 | ||
|
|
c252d2d554 | ||
|
|
fdb1697ead | ||
|
|
045784f1f9 | ||
|
|
ef13f128ed | ||
|
|
ef46ac9d0e | ||
|
|
b430e4929b | ||
|
|
831fe185e3 | ||
|
|
fdb9c08895 | ||
|
|
abe0a37aca | ||
|
|
9f86286e57 | ||
|
|
9ada74d70b | ||
|
|
855f59b407 | ||
|
|
edcdbcc577 | ||
|
|
a24daad15e | ||
|
|
203f5418fa | ||
|
|
30c37a1fa4 | ||
|
|
863bfb5cf4 | ||
|
|
f8bc0835d0 | ||
|
|
bef32197da | ||
|
|
1b08627c44 | ||
|
|
501cf8869d | ||
|
|
5d272ce420 | ||
|
|
e15762c725 | ||
|
|
a8f54306aa | ||
|
|
664412553d | ||
|
|
b512a35e3d | ||
|
|
6e3268ca17 | ||
|
|
03a2bcf37e | ||
|
|
6f65d127aa | ||
|
|
1034c44ee3 | ||
|
|
4ec6fe308c | ||
|
|
01b05b0bda | ||
|
|
d377fc409f | ||
|
|
c2c0c7e292 | ||
|
|
8609a6acb7 | ||
|
|
7142567848 | ||
|
|
58c93576b2 | ||
|
|
5a4972f940 | ||
|
|
64f94a11cd | ||
|
|
45745ed381 | ||
|
|
6834b664fd | ||
|
|
f03fbbfafe | ||
|
|
ef52f691cf | ||
|
|
273fdfb082 | ||
|
|
1de8c6998a | ||
|
|
cc21ff1863 | ||
|
|
ea0f4d091a | ||
|
|
4a43eabed2 | ||
|
|
9d18de1110 | ||
|
|
87984cdaa8 | ||
|
|
ab5d8ce28a | ||
|
|
118af53cc8 | ||
|
|
d7564d716f | ||
|
|
69d8523b85 | ||
|
|
535cf3b3ba | ||
|
|
ddaffd166a | ||
|
|
7bec8e42ce | ||
|
|
5e3f538674 | ||
|
|
530ccca1e4 | ||
|
|
f1a7cfb48c | ||
|
|
6b9fec8ef7 | ||
|
|
322311bda3 | ||
|
|
643db859a3 | ||
|
|
e374a203ff | ||
|
|
21658b19bc | ||
|
|
d6b439f097 | ||
|
|
74c6be70f7 | ||
|
|
8dd99376fd | ||
|
|
653bf17535 | ||
|
|
4b100d0073 | ||
|
|
5a0d267770 | ||
|
|
adb0442da5 | ||
|
|
674735ca65 | ||
|
|
ae31a360e9 | ||
|
|
2188e9000e | ||
|
|
1623d208f7 | ||
|
|
18e9c91d69 | ||
|
|
2dd0850854 | ||
|
|
65f6e436f3 | ||
|
|
e07b15cd00 | ||
|
|
7de16ec735 | ||
|
|
f18d4f1915 | ||
|
|
855393e137 | ||
|
|
7488330980 | ||
|
|
3ee3e1da5c | ||
|
|
ce48913e43 | ||
|
|
c9faec6c37 | ||
|
|
79041e5f40 | ||
|
|
5d03bdf561 | ||
|
|
8359d2191a | ||
|
|
2f0874531e | ||
|
|
b43cdabd46 | ||
|
|
4aee3477a5 | ||
|
|
2f027e5fa1 | ||
|
|
b6df3aaf38 | ||
|
|
dace5d545a | ||
|
|
0f07854917 | ||
|
|
8efaec8c6d | ||
|
|
ed5e388e7d | ||
|
|
11d1f3f1ce | ||
|
|
0b5d69bf28 | ||
|
|
636e926ece | ||
|
|
c57248363e | ||
|
|
4f88a2e554 | ||
|
|
4b13c38c02 | ||
|
|
c826822ea5 | ||
|
|
ed204b9111 | ||
|
|
2f05508385 | ||
|
|
1108b33f7c | ||
|
|
31e8a8e302 | ||
|
|
0eaa5f458b | ||
|
|
d065e91626 | ||
|
|
eca6f74545 | ||
|
|
39c08c8201 | ||
|
|
01fbe1cf24 | ||
|
|
5d5d82b3d6 | ||
|
|
d213e91260 | ||
|
|
434cc3836a | ||
|
|
3f3036c8cc | ||
|
|
51a98c8e9f | ||
|
|
726e7c3ef1 | ||
|
|
cc7e931682 | ||
|
|
8c35ebdb2c | ||
|
|
00a4561a4b | ||
|
|
181a5207e9 | ||
|
|
4814a552c7 | ||
|
|
46b84fc6a8 | ||
|
|
bc92ae8319 | ||
|
|
d5e857f994 | ||
|
|
bed41badbc | ||
|
|
c4e8692564 | ||
|
|
0bef366685 | ||
|
|
dc072e21af | ||
|
|
fafb90d2d2 | ||
|
|
7222716957 | ||
|
|
ee172e50ed | ||
|
|
f6dc3dfc74 | ||
|
|
80b44941d0 | ||
|
|
73ecb64a9d | ||
|
|
a8502b63b2 | ||
|
|
f120d12939 | ||
|
|
81abf8925b | ||
|
|
dc9aceb1cf | ||
|
|
e70685a166 | ||
|
|
850f322dff | ||
|
|
50d6e0dc1d | ||
|
|
48bf133e46 | ||
|
|
002f024926 | ||
|
|
d86c0a8ba8 | ||
|
|
7603ea0371 | ||
|
|
c97ffbb99e | ||
|
|
dee74fbe03 | ||
|
|
4cf3a53b4f | ||
|
|
2b72e5ea0b | ||
|
|
76c3de4ace | ||
|
|
a89fa7afb0 | ||
|
|
424c388ca3 | ||
|
|
5f1859fff5 | ||
|
|
5af552721c | ||
|
|
3818cbc7cb | ||
|
|
31e5a695d7 | ||
|
|
b7470581a9 | ||
|
|
2a36c6ea6e | ||
|
|
99fefbfb44 | ||
|
|
f045f4eb38 | ||
|
|
2f130502cc | ||
|
|
4b0f123313 | ||
|
|
f69edff927 | ||
|
|
246535f3f1 | ||
|
|
efaab0c83b | ||
|
|
669660a49c | ||
|
|
3651e58631 | ||
|
|
2ba90838e4 | ||
|
|
72ded26393 | ||
|
|
111e0914b5 | ||
|
|
2bfa24e72c | ||
|
|
bf61dd030a | ||
|
|
592143137f | ||
|
|
999ad82e22 | ||
|
|
1f95368168 | ||
|
|
bce8d05170 | ||
|
|
7d72d4cc9c | ||
|
|
3285b20d98 | ||
|
|
8cd92cc8c3 | ||
|
|
6a993f765e | ||
|
|
565fb896bd | ||
|
|
d6cff70840 | ||
|
|
56f57e2ad6 | ||
|
|
961f6059b7 | ||
|
|
2f34b6271e | ||
|
|
2c97bce3c0 | ||
|
|
ed8cad319d | ||
|
|
8e39fe29ba | ||
|
|
1ee72b03b4 | ||
|
|
92ad2b4a0d | ||
|
|
61d500142c | ||
|
|
cf361902c2 | ||
|
|
07d6293173 | ||
|
|
c446321173 | ||
|
|
17d59c1e15 | ||
|
|
097f4c90fd | ||
|
|
87670bfe52 | ||
|
|
f5ba140830 | ||
|
|
854051b42f | ||
|
|
076fc443d6 | ||
|
|
43a5e10d72 | ||
|
|
2f27e82a7b | ||
|
|
d2871d7bc9 | ||
|
|
4171eafeb9 | ||
|
|
90a1ce0958 | ||
|
|
3a75c5c306 | ||
|
|
ebc5b6409b | ||
|
|
e8314bfced | ||
|
|
06abcc0905 | ||
|
|
168af9eb81 | ||
|
|
f57a8862f6 | ||
|
|
e6cbb52111 | ||
|
|
238006dbb2 | ||
|
|
3366f6b10c | ||
|
|
8706ecab72 | ||
|
|
2ab8e2f981 | ||
|
|
958a220163 | ||
|
|
62a5b55303 | ||
|
|
4ed0cea596 | ||
|
|
c7676f9e63 | ||
|
|
8cbd780479 | ||
|
|
c999b53691 | ||
|
|
af4f1a5533 | ||
|
|
2904ba74dc | ||
|
|
d97a3af401 | ||
|
|
c8d2d7ce87 | ||
|
|
b0e5224baa | ||
|
|
34546e5528 | ||
|
|
7cb3a4981e | ||
|
|
85a11e4195 | ||
|
|
8d82b68ab5 | ||
|
|
c778c98d64 | ||
|
|
44cbb43472 | ||
|
|
107ccce58a | ||
|
|
8fb4599a06 | ||
|
|
db7522970f | ||
|
|
0b8253b8e4 | ||
|
|
8f38a35479 | ||
|
|
5b5c805bb0 | ||
|
|
6fcb411d80 | ||
|
|
d8ca70b5cf | ||
|
|
7a6cdc9460 | ||
|
|
eb0d034a46 | ||
|
|
de3c9e7337 | ||
|
|
bb86520f43 | ||
|
|
2e3626069e | ||
|
|
b395d71a3c | ||
|
|
f7843cb5b2 | ||
|
|
9b0e042add | ||
|
|
5bde8145a2 | ||
|
|
99ddaec57a | ||
|
|
eb46ac3d39 | ||
|
|
8aead373d1 | ||
|
|
c79d79e4ce | ||
|
|
090dc09d39 | ||
|
|
9432deb0b9 | ||
|
|
cd1dd82978 | ||
|
|
f9fafb844b | ||
|
|
bdac4492ac | ||
|
|
78e1f711c4 | ||
|
|
9cfc88b96f | ||
|
|
c53ca529a8 | ||
|
|
b8e5bae524 | ||
|
|
904e1f9dc7 | ||
|
|
655aedc478 | ||
|
|
1d69805fac | ||
|
|
bf75ef1c36 |
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -7,9 +7,9 @@ body:
|
||||
attributes:
|
||||
label: Checklist before submitting an issue
|
||||
options:
|
||||
- label: I have searched through the existing [closed and open issues](https://github.com/elkowar/eww/issues?q=is%3Aissue) for eww and made sure this is not a duplicate
|
||||
- label: I have searched through the existing [closed and open issues](https://github.com/Ewwii-sh/ewwii/issues?q=is%3Aissue) for ewwii and made sure this is not a duplicate
|
||||
required: true
|
||||
- label: I have specifically verified that this bug is not a common [user error](https://github.com/elkowar/eww/issues?q=is%3Aissue+label%3Ano-actual-bug+is%3Aclosed)
|
||||
- label: I have specifically verified that this bug is not a common [user error](https://github.com/Ewwii-sh/ewwii/issues?q=is%3Aissue+label%3Ano-actual-bug+is%3Aclosed)
|
||||
required: true
|
||||
- label: I am providing as much relevant information as I am able to in this bug report (Minimal config to reproduce the issue for example, if applicable)
|
||||
required: true
|
||||
@@ -40,6 +40,6 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Platform and environment"
|
||||
description: "Does this happen on wayland, X11, or on both? What WM/Compositor are you using? Which version of eww are you using? (when using a git version, optimally provide the exact commit ref)."
|
||||
description: "Does this happen on wayland, X11, or on both? What WM/Compositor are you using? Which version of ewwii are you using? (when using a git version, optimally provide the exact commit ref)."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -12,7 +12,7 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Proposed configuration syntax"
|
||||
description: "If the feature you are requesting would add or change something to the Eww configuration, please provide an example for how the feature could be used."
|
||||
description: "If the feature you are requesting would add or change something to the Ewwii configuration, please provide an example for how the feature could be used."
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
|
||||
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@@ -26,5 +26,4 @@ Please make sure you can check all the boxes that apply to this PR.
|
||||
|
||||
- [ ] All widgets I've added are correctly documented.
|
||||
- [ ] I added my changes to CHANGELOG.md, if appropriate.
|
||||
- [ ] The documentation in the `docs/content/main` directory has been adjusted to reflect my changes.
|
||||
- [ ] I used `cargo fmt` to automatically format all code before committing
|
||||
|
||||
73
.github/workflows/build.yml
vendored
73
.github/workflows/build.yml
vendored
@@ -1,45 +1,56 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get update && sudo apt-get install libgtk-3-dev libgtk-layer-shell-dev libdbusmenu-gtk3-dev
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: archlinux:latest
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pacman -Syu --noconfirm
|
||||
pacman -S --noconfirm base-devel gtk4 gtk4-layer-shell pkgconf git
|
||||
|
||||
- name: Setup rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy,rustfmt
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Load rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Setup rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy,rustfmt
|
||||
|
||||
- name: Setup problem matchers
|
||||
uses: r7kamura/rust-problem-matchers@v1
|
||||
- name: Load rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Check formatting
|
||||
run: cargo fmt -- --check
|
||||
- name: Check with default features
|
||||
run: cargo check
|
||||
- name: Setup problem matchers
|
||||
uses: r7kamura/rust-problem-matchers@v1
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test
|
||||
- name: Check formatting
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- name: Check x11 only
|
||||
run: cargo check --no-default-features --features=x11
|
||||
- name: Check wayland only
|
||||
run: cargo check --no-default-features --features=wayland
|
||||
- name: Check no-backend
|
||||
run: cargo check --no-default-features
|
||||
- name: Check with default features
|
||||
run: cargo check
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test
|
||||
|
||||
- name: Check x11 only
|
||||
run: cargo check --no-default-features --features=x11
|
||||
|
||||
- name: Check wayland only
|
||||
run: cargo check --no-default-features --features=wayland
|
||||
|
||||
- name: Check no-backend
|
||||
run: cargo check --no-default-features
|
||||
|
||||
117
CHANGELOG.md
117
CHANGELOG.md
@@ -5,6 +5,123 @@ All notable changes to `ewwii` are documented here.
|
||||
This changelog follows the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format,
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
### Added
|
||||
|
||||
- `gtk_ui` function for loading .ui files.
|
||||
- `widget-control` (`wc` in short) command for controlling widgets.
|
||||
- `placeholder` property to input widget.
|
||||
- `transition_duration` property to stack widget.
|
||||
- `widget_control` utility function for dynamic widget handling.
|
||||
- `text` and `show_text` property to progressbar widget.
|
||||
- `graph` widget back.
|
||||
|
||||
### Changed
|
||||
|
||||
- `v4_18` feature flag for to the `gtk4` crate to enable newer APIs.
|
||||
- `content_fit` property to image widget.
|
||||
- `can_shrink` property to image widget.
|
||||
- `mutations` property to localsignal.
|
||||
- `eval_ignore` property to all widgets.
|
||||
- Touch support to scale widget.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `clockwise` property not working on circular_progress.
|
||||
|
||||
### Removed
|
||||
|
||||
- icon widget.
|
||||
|
||||
## [0.3.1] - 2025-11-01
|
||||
|
||||
## Fixed
|
||||
|
||||
- Circular progress bar not updating dynamically.
|
||||
- LocalSignal values not getting transformed to suite property type.
|
||||
- LocalBind not finding properties of range subclasses.
|
||||
|
||||
## [0.3.0] - 2025-11-01
|
||||
|
||||
### Added
|
||||
|
||||
- `localsignal` signal for fast and cheap property update.
|
||||
- `localbind` utility for binding `localsignal` to a widget property.
|
||||
- `onkeypress` property to eventbox.
|
||||
- `onkeyrelease` property to eventbox.
|
||||
- Selection of dash as shell if it installed.
|
||||
- `--with-plugin` flag for `daemon` command.
|
||||
- `register_function` API in ewwii_plugin_api for registering functions that rhai can call to.
|
||||
- `slib` rhai module for calling functions registered via `register_function`.
|
||||
- `orientation` property to eventbox.
|
||||
- `spacing` property to eventbox.
|
||||
- `space_evenly` property to eventbox.
|
||||
- An advanced widget named `flowbox`.
|
||||
- `focusable` property to all widget.
|
||||
- `widget_name` property to all widget.
|
||||
- `lifetime` flag for update command.
|
||||
- `circular-progress` widget back.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Old widget creeping into new ones after hot reload.
|
||||
- Ewwii defaulting to default gtk4 theme. #9
|
||||
- Few issues with eventbox drop target.
|
||||
- Poll/Listen variables not exposed in other modules on first launch.
|
||||
- Not able to define poll/listen variables in other files.
|
||||
- Overlay and Tooltip widgets not being reactive.
|
||||
|
||||
## [0.3.0-beta] - 2025-10-11
|
||||
|
||||
### Added
|
||||
|
||||
- Support for binary level plugins.
|
||||
- `set-plugin` command to load shared libraries (i.e plugins).
|
||||
- `ewwii_plugin_api` crate for building ewwii plugins easily.
|
||||
|
||||
### Changed
|
||||
|
||||
- The name of `slider` widget to `scale`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `focusable` property not working issue.
|
||||
- Poll/Listen variable scope not accessable to all modules.
|
||||
|
||||
## [0.3.0-alpha] - 2025-10-04
|
||||
|
||||
### Added
|
||||
|
||||
- GTK4 support.
|
||||
- `can_target` boolean property for all widgets.
|
||||
- `icon` widget which always preserves a scaling of 1:1.
|
||||
- Full native wayland compatibility (x11 compatibility has decreased).
|
||||
|
||||
### Changed
|
||||
|
||||
- The way dynamic updates are handled.
|
||||
- Window positioning logic.
|
||||
- X11 communication and x11 based window handling.
|
||||
- Daemon GTK main loop.
|
||||
- Application of sticky and stacking propery of window on X11.
|
||||
- The full implementation of eventbox widget.
|
||||
- GTK controller/signal handling.
|
||||
- Initialization logic of window.
|
||||
- Overlay widget dynamic post-creation re-creation logic (bugs are expected).
|
||||
|
||||
### Removed
|
||||
|
||||
- `show_details` property of calendar.
|
||||
- `angle` property from label.
|
||||
- `icon_size` property from image widget.
|
||||
- `same_size` property from stack widget (box is a replacement).
|
||||
- `icon_name` from `image` widget as a result of the introduction of `icon` widget.
|
||||
- `transform`, `graph`, and `circular_progress` widget (temporarily).
|
||||
- Application of css class on the window.
|
||||
- Legacy GTK3 related code.
|
||||
- Centerbox widget.
|
||||
|
||||
## [0.2.0] - 2025-09-29
|
||||
|
||||
### Added
|
||||
|
||||
681
Cargo.lock
generated
681
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
@@ -1,11 +1,13 @@
|
||||
[workspace]
|
||||
members = ["crates/*", "tools/*"]
|
||||
members = ["crates/*", "tools/*", "proc_macros/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
|
||||
shared_utils = { version = "0.1.0", path = "crates/shared_utils" }
|
||||
rhai_impl = { version = "0.1.0", path = "crates/rhai_impl" }
|
||||
scan_prop_proc = { version = "0.1.0", path = "proc_macros/scan_prop_proc" }
|
||||
ewwii_plugin_api = { version = "0.7.0", path = "crates/ewwii_plugin_api" }
|
||||
|
||||
anyhow = "1.0.86"
|
||||
ahash = "0.8.12"
|
||||
@@ -24,7 +26,7 @@ derive_more = { version = "1", features = [
|
||||
extend = "1.2"
|
||||
futures = "0.3.30"
|
||||
grass = "0.13.4"
|
||||
gtk = "0.18.1"
|
||||
gtk4 = { version = "0.10.3", features = ["v4_18", "v4_8"] }
|
||||
itertools = "0.13.0"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
@@ -34,7 +36,7 @@ once_cell = "1.19"
|
||||
pretty_assertions = "1.4.0"
|
||||
pretty_env_logger = "0.5.0"
|
||||
regex = "1.10.5"
|
||||
rhai = "1.22.2"
|
||||
rhai = "1.23.6"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
simple-signal = "1.1"
|
||||
@@ -44,6 +46,10 @@ thiserror = "1.0"
|
||||
tokio = { version = "1.39.2", features = ["full"] }
|
||||
unescape = "0.1"
|
||||
wait-timeout = "0.2"
|
||||
syn = "2.0.107"
|
||||
quote = "1.0.41"
|
||||
proc-macro2 = "1.0.101"
|
||||
shell-words = "1.1.0"
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
|
||||
24
README.md
24
README.md
@@ -1,4 +1,5 @@
|
||||
[](https://deps.rs/repo/github/byson94/ewwii)
|
||||
[](https://ewwii-sh.github.io/docs)
|
||||
|
||||
# Ewwii
|
||||
|
||||
@@ -8,18 +9,23 @@ Elkowars Wacky Widgets Imporved Interface is a fork of Elkowars Wacky Widgets wh
|
||||
|
||||
## Examples
|
||||
|
||||
All examples are in the [examples directory](./examples/).
|
||||
Examples of projects powered by ewwii.
|
||||
|
||||
- A basic bar [CLICK TO SEE](./examples/ewwii-bar) <br>
|
||||

|
||||
| Project | Preview |
|
||||
|---------|---------|
|
||||
| **Basic Bar**<br>[- View Example](./examples/ewwii-bar) | [](./examples/ewwii-bar) |
|
||||
| **Data Structures**<br>[- View Example](./examples/data-structures) | [](./examples/data-structures) |
|
||||
| **Wi-Fi GUI Template**<br>[- View on GitHub](https://github.com/Ewwii-sh/ewifi_gui_template) |  |
|
||||
| **Obsidian Bar Template**<br>[- View on GitHub](https://github.com/Ewwii-sh/obsidian-bar) | [](https://github.com/Ewwii-sh/obsidian-bar) |
|
||||
| **Binary Dots by [@BinaryHarbinger](https://github.com/BinaryHarbinger)**<br>[- View on GitHub](https://github.com/BinaryHarbinger/binarydots/) | [](https://github.com/BinaryHarbinger/binarydots)
|
||||
| **Astatine Dots (Linux Rice with Ewwii)**<br>[- View on GitHub](https://github.com/Ewwii-sh/astatine-dots) | [](https://github.com/Ewwii-sh/astatine-dots) |
|
||||
|
||||
- Data structures [CLICK TO SEE](./examples/data-structures) <br>
|
||||

|
||||
## Features
|
||||
|
||||
## Templates
|
||||
|
||||
- A wifi gui template [CLICK TO SEE](https://github.com/Ewwii-sh/ewifi_gui_template) <br>
|
||||
<img src="https://raw.githubusercontent.com/Ewwii-sh/ewwii/main/docs/src/images/wifi_manager_template.png" width="400" />
|
||||
- Powered by Gtk4
|
||||
- Supports Hot reload
|
||||
- Extensibility via plugins and rhai modules
|
||||
- X11 + Wayland support
|
||||
|
||||
## Contribewwtiing
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ewwii"
|
||||
version = "0.2.0"
|
||||
version = "0.4.0"
|
||||
authors = ["byson94 <byson94wastaken@gmail.com>"]
|
||||
description = "Widgets for everyone made better!"
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -11,17 +11,17 @@ edition = "2021"
|
||||
|
||||
[features]
|
||||
default = ["x11", "wayland"]
|
||||
x11 = ["gdkx11", "x11rb"]
|
||||
wayland = ["gtk-layer-shell"]
|
||||
x11 = ["gdk4-x11", "x11rb"]
|
||||
wayland = ["gtk4-layer-shell"]
|
||||
|
||||
[dependencies]
|
||||
shared_utils.workspace = true
|
||||
rhai_impl.workspace = true
|
||||
ewwii_plugin_api.workspace = true
|
||||
|
||||
gtk-layer-shell = { version = "0.8.1", optional = true, features=["v0_6"] }
|
||||
gdkx11 = { version = "0.18", optional = true }
|
||||
gtk4-layer-shell = { version = "0.6.3", optional = true }
|
||||
gdk4-x11 = { version = "0.10.1", optional = true }
|
||||
x11rb = { version = "0.13.1", features = ["randr"], optional = true }
|
||||
gdk-sys = "0.18.0"
|
||||
|
||||
grass.workspace = true
|
||||
thiserror.workspace = true
|
||||
@@ -35,7 +35,7 @@ derive_more.workspace = true
|
||||
extend.workspace = true
|
||||
futures.workspace = true
|
||||
smart-default.workspace = true
|
||||
gtk.workspace = true
|
||||
gtk4.workspace = true
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
nix = { workspace = true, features = ["process", "fs", "signal"] }
|
||||
@@ -49,7 +49,10 @@ simple-signal.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
unescape.workspace = true
|
||||
wait-timeout.workspace = true
|
||||
rhai.workspace = true
|
||||
rhai = { workspace = true, features = ["internals"] }
|
||||
shell-words.workspace = true
|
||||
# Plugin loading
|
||||
libloading = "0.8.9"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::diag_error::DiagError;
|
||||
use crate::plugin::PluginRequest;
|
||||
use crate::{
|
||||
daemon_response::DaemonResponseSender,
|
||||
display_backend::DisplayBackend,
|
||||
error_handling_ctx,
|
||||
gtk::prelude::{
|
||||
ContainerExt, CssProviderExt, GtkWindowExt, MonitorExt, StyleContextExt, WidgetExt,
|
||||
gtk4::prelude::{
|
||||
Cast, CastNone, DisplayExt, GtkWindowExt, ListModelExt, MonitorExt, NativeExt, ObjectExt,
|
||||
StyleContextExt, WidgetExt,
|
||||
},
|
||||
paths::EwwiiPaths,
|
||||
widgets::window::Window,
|
||||
// dynval::DynVal,
|
||||
widgets::{
|
||||
build_widget::build_gtk_widget, build_widget::WidgetInput,
|
||||
@@ -23,16 +23,16 @@ use crate::{
|
||||
*,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use codespan_reporting::files::Files;
|
||||
use ewwii_plugin_api as epapi;
|
||||
use gdk::Monitor;
|
||||
use glib::ObjectExt;
|
||||
use gtk::{gdk, glib};
|
||||
use gtk4::Window;
|
||||
use gtk4::{gdk, glib};
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use once_cell::sync::OnceCell;
|
||||
use rhai::Dynamic;
|
||||
use rhai_impl::ast::WidgetNode;
|
||||
use rhai_impl::parser::ParseConfig;
|
||||
use serde::{de::Error as SerdeError, Deserialize, Deserializer};
|
||||
use shared_utils::Span;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::{HashMap, HashSet},
|
||||
@@ -42,6 +42,12 @@ use std::{
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
static ACTIVE_PLUGIN: OnceCell<libloading::Library> = OnceCell::new();
|
||||
|
||||
fn set_active_plugin(lib: libloading::Library) -> Result<()> {
|
||||
ACTIVE_PLUGIN.set(lib).map_err(|_| anyhow!("Plugin already set"))
|
||||
}
|
||||
|
||||
/// A command for the ewwii daemon.
|
||||
/// While these are mostly generated from ewwii CLI commands (see [`opts::ActionWithServer`]),
|
||||
/// they may also be generated from other places internally.
|
||||
@@ -79,9 +85,14 @@ pub enum DaemonCommand {
|
||||
ShowState(DaemonResponseSender),
|
||||
ListWindows(DaemonResponseSender),
|
||||
ListActiveWindows(DaemonResponseSender),
|
||||
WidgetControl {
|
||||
action: crate::opts::WidgetControlAction,
|
||||
sender: DaemonResponseSender,
|
||||
},
|
||||
TriggerUpdateUI {
|
||||
inject_vars: Option<HashMap<String, String>>,
|
||||
should_preserve_state: bool,
|
||||
lifetime: Option<String>,
|
||||
sender: DaemonResponseSender,
|
||||
},
|
||||
CallRhaiFns {
|
||||
@@ -93,6 +104,10 @@ pub enum DaemonCommand {
|
||||
print: bool,
|
||||
sender: DaemonResponseSender,
|
||||
},
|
||||
SetPlugin {
|
||||
file_path: String,
|
||||
sender: DaemonResponseSender,
|
||||
},
|
||||
}
|
||||
|
||||
/// An opened window.
|
||||
@@ -118,17 +133,17 @@ impl std::fmt::Debug for EwwiiWindow {
|
||||
impl EwwiiWindow {
|
||||
/// Close the GTK window and disconnect the destroy event-handler.
|
||||
///
|
||||
/// You need to make sure that the scope get's properly cleaned from the state graph
|
||||
/// and that script-vars get cleaned up properly
|
||||
/// You need to make sure that the window gets propery cleaned from gtk4!
|
||||
pub fn close(self) {
|
||||
log::info!("Closing gtk window {}", self.name);
|
||||
self.gtk_window.close();
|
||||
|
||||
for handler_id_opt in [self.destroy_event_handler_id, self.delete_event_handler_id] {
|
||||
if let Some(handler_id) = handler_id_opt {
|
||||
self.gtk_window.disconnect(handler_id);
|
||||
}
|
||||
}
|
||||
|
||||
self.gtk_window.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +158,8 @@ pub struct App<B: DisplayBackend> {
|
||||
/// Window names that are supposed to be open, but failed.
|
||||
/// When reloading the config, these should be opened again.
|
||||
pub failed_windows: HashSet<String>,
|
||||
pub css_provider: gtk::CssProvider,
|
||||
pub css_provider: gtk4::CssProvider,
|
||||
pub reloading: bool,
|
||||
|
||||
/// Sender to send [`DaemonCommand`]s
|
||||
pub app_evt_send: UnboundedSender<DaemonCommand>,
|
||||
@@ -155,11 +171,14 @@ pub struct App<B: DisplayBackend> {
|
||||
pub widget_reg_store: Rc<Mutex<Option<WidgetRegistry>>>,
|
||||
|
||||
// The cached store of poll/listen handlers
|
||||
pub pl_handler_store: Option<rhai_impl::updates::ReactiveVarStore>,
|
||||
pub pl_handler_store: rhai_impl::updates::ReactiveVarStore,
|
||||
pub clear_pl_onclose: HashMap<String, String>,
|
||||
|
||||
pub rt_engine_config: EngineConfValues,
|
||||
pub config_parser: Rc<RefCell<ParseConfig>>,
|
||||
|
||||
pub paths: EwwiiPaths,
|
||||
pub gtk_main_loop: gtk4::glib::MainLoop,
|
||||
pub phantom: PhantomData<B>,
|
||||
}
|
||||
|
||||
@@ -180,8 +199,18 @@ async fn wait_for_monitor_model() {
|
||||
let display = gdk::Display::default().expect("could not get default display");
|
||||
let start = std::time::Instant::now();
|
||||
loop {
|
||||
let all_monitors_set = (0..display.n_monitors())
|
||||
.all(|i| display.monitor(i).and_then(|monitor| monitor.model()).is_some());
|
||||
let monitors_model = display.monitors();
|
||||
let n_monitors = monitors_model.n_items();
|
||||
|
||||
let all_monitors_set = (0..n_monitors).all(|i| {
|
||||
if let Some(obj) = monitors_model.item(i) {
|
||||
// Downcast GObject to Monitor
|
||||
let monitor: Monitor = obj.downcast().unwrap();
|
||||
monitor.model().is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if all_monitors_set {
|
||||
break;
|
||||
}
|
||||
@@ -207,7 +236,7 @@ impl<B: DisplayBackend> App<B> {
|
||||
match event {
|
||||
DaemonCommand::NoOp => {}
|
||||
DaemonCommand::OpenInspector => {
|
||||
gtk::Window::set_interactive_debugging(true);
|
||||
gtk4::Window::set_interactive_debugging(true);
|
||||
}
|
||||
DaemonCommand::ReloadConfigAndCss(sender) => {
|
||||
// Wait for all monitor models to be set. When a new monitor gets added, this
|
||||
@@ -216,7 +245,11 @@ impl<B: DisplayBackend> App<B> {
|
||||
wait_for_monitor_model().await;
|
||||
let mut errors = Vec::new();
|
||||
|
||||
let config_result = config::read_from_ewwii_paths(&self.paths);
|
||||
let mut parser_ref = self.config_parser.borrow_mut();
|
||||
let config_result = config::read_from_ewwii_paths(&self.paths, &mut *parser_ref);
|
||||
|
||||
drop(parser_ref);
|
||||
|
||||
if let Err(e) = config_result.and_then(|new_config| self.load_config(new_config)) {
|
||||
errors.push(e)
|
||||
}
|
||||
@@ -320,17 +353,22 @@ impl<B: DisplayBackend> App<B> {
|
||||
sender.send_success(output)?
|
||||
}
|
||||
DaemonCommand::ShowState(sender) => {
|
||||
if let Some(maybe_store) = &self.pl_handler_store {
|
||||
let output = format!("{:#?}", maybe_store.read().unwrap());
|
||||
sender.send_success(output)?
|
||||
} else {
|
||||
sender.send_failure(
|
||||
"The poll/listen handler store doesn't exist or is empty".to_string(),
|
||||
)?
|
||||
}
|
||||
let output = format!("{:#?}", &self.pl_handler_store.read().unwrap());
|
||||
sender.send_success(output)?
|
||||
}
|
||||
DaemonCommand::TriggerUpdateUI { inject_vars, should_preserve_state, sender } => {
|
||||
match self.trigger_ui_update_with(inject_vars, should_preserve_state) {
|
||||
DaemonCommand::TriggerUpdateUI {
|
||||
inject_vars,
|
||||
should_preserve_state,
|
||||
lifetime,
|
||||
sender,
|
||||
} => {
|
||||
match self.trigger_ui_update_with(inject_vars, should_preserve_state, lifetime) {
|
||||
Ok(_) => sender.send_success(String::new())?,
|
||||
Err(e) => sender.send_failure(e.to_string())?,
|
||||
};
|
||||
}
|
||||
DaemonCommand::WidgetControl { action, sender } => {
|
||||
match self.perform_widget_control(action) {
|
||||
Ok(_) => sender.send_success(String::new())?,
|
||||
Err(e) => sender.send_failure(e.to_string())?,
|
||||
};
|
||||
@@ -353,6 +391,12 @@ impl<B: DisplayBackend> App<B> {
|
||||
Err(e) => sender.send_failure(e.to_string())?,
|
||||
};
|
||||
}
|
||||
DaemonCommand::SetPlugin { file_path, sender } => {
|
||||
match self.set_ewwii_plugin(file_path) {
|
||||
Ok(_) => sender.send_success(String::from("OK"))?,
|
||||
Err(e) => sender.send_failure(e.to_string())?,
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -364,7 +408,7 @@ impl<B: DisplayBackend> App<B> {
|
||||
for (_, window) in self.open_windows.drain() {
|
||||
window.close();
|
||||
}
|
||||
gtk::main_quit();
|
||||
self.gtk_main_loop.quit();
|
||||
let _ = crate::application_lifecycle::send_exit();
|
||||
}
|
||||
|
||||
@@ -379,6 +423,9 @@ impl<B: DisplayBackend> App<B> {
|
||||
|
||||
// let scope_index = ewwii_window.scope_index;
|
||||
ewwii_window.close();
|
||||
if let Some(var_name) = self.clear_pl_onclose.remove(instance_id) {
|
||||
self.pl_handler_store.write().unwrap().remove(&var_name);
|
||||
}
|
||||
|
||||
if auto_reopen {
|
||||
self.failed_windows.insert(instance_id.to_string());
|
||||
@@ -393,7 +440,7 @@ impl<B: DisplayBackend> App<B> {
|
||||
}
|
||||
|
||||
// stop poll/listen handlers if no windows are open
|
||||
if self.open_windows.is_empty() {
|
||||
if self.open_windows.is_empty() || self.reloading {
|
||||
rhai_impl::updates::kill_state_change_handler();
|
||||
}
|
||||
|
||||
@@ -458,7 +505,6 @@ impl<B: DisplayBackend> App<B> {
|
||||
|
||||
let monitor = get_gdk_monitor(initiator.monitor.clone())?;
|
||||
let mut ewwii_window = initialize_window::<B>(&initiator, monitor, root_widget)?;
|
||||
ewwii_window.gtk_window.style_context().add_class(window_name);
|
||||
|
||||
// Start the poll/listen only once per startup
|
||||
// at the start, the open_windows will be empty because
|
||||
@@ -471,23 +517,35 @@ impl<B: DisplayBackend> App<B> {
|
||||
// so I guess that I will just let it stay right here.
|
||||
let config_path = self.paths.get_rhai_path();
|
||||
let compiled_ast = self.ewwii_config.get_owned_compiled_ast();
|
||||
let mut stored_parser = rhai_impl::parser::ParseConfig::new();
|
||||
stored_parser.set_opt_level(get_opt_level_from(
|
||||
self.rt_engine_config.optimization_level.unwrap_or(1),
|
||||
));
|
||||
|
||||
if self.open_windows.is_empty() {
|
||||
{
|
||||
let mut stored_parser = self.config_parser.borrow_mut();
|
||||
stored_parser.set_opt_level(get_opt_level_from(
|
||||
self.rt_engine_config.optimization_level.unwrap_or(1),
|
||||
));
|
||||
}
|
||||
|
||||
let stored_parser_clone = self.config_parser.clone();
|
||||
if self.open_windows.is_empty() || self.reloading {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<String>();
|
||||
let widget_reg_store = self.widget_reg_store.clone();
|
||||
|
||||
let store = rhai_impl::updates::handle_state_changes(
|
||||
// Will automatically mutate pl_handler_store
|
||||
rhai_impl::updates::handle_state_changes(
|
||||
self.ewwii_config.get_root_node()?.as_ref(),
|
||||
tx,
|
||||
self.pl_handler_store.clone(),
|
||||
);
|
||||
|
||||
self.pl_handler_store = Some(store.clone());
|
||||
let store = self.pl_handler_store.clone();
|
||||
let b_interval = self.rt_engine_config.batching_interval;
|
||||
|
||||
// kick start the localsignal
|
||||
rhai_impl::updates::handle_localsignal_changes(
|
||||
stored_parser_clone.clone(),
|
||||
compiled_ast.clone(),
|
||||
);
|
||||
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
let mut pending_updates = HashSet::new();
|
||||
|
||||
@@ -507,11 +565,13 @@ impl<B: DisplayBackend> App<B> {
|
||||
}
|
||||
|
||||
let vars = store.read().unwrap().clone();
|
||||
let mut parser_rc = stored_parser_clone.borrow_mut();
|
||||
let compiled_ast_ref = compiled_ast.as_ref().map(|rc| rc.borrow());
|
||||
match generate_new_widgetnode(
|
||||
&vars,
|
||||
&config_path,
|
||||
compiled_ast.as_deref(),
|
||||
Some(&mut stored_parser),
|
||||
compiled_ast_ref.as_deref(),
|
||||
&mut *parser_rc,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -539,18 +599,20 @@ impl<B: DisplayBackend> App<B> {
|
||||
} else {
|
||||
// else, just update the window from the current store.
|
||||
let widget_reg_store = self.widget_reg_store.clone();
|
||||
let store = self
|
||||
.pl_handler_store
|
||||
.clone()
|
||||
.expect("Failed to clone poll listen handler store");
|
||||
let store = self.pl_handler_store.clone();
|
||||
|
||||
// notifiy localsignals
|
||||
rhai_impl::updates::notify_all_localsignals();
|
||||
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
let vars = store.read().unwrap().clone();
|
||||
let mut parser_rc = stored_parser_clone.borrow_mut();
|
||||
let compiled_ast_ref = compiled_ast.as_ref().map(|rc| rc.borrow());
|
||||
match generate_new_widgetnode(
|
||||
&vars,
|
||||
&config_path,
|
||||
compiled_ast.as_deref(),
|
||||
Some(&mut stored_parser),
|
||||
compiled_ast_ref.as_deref(),
|
||||
&mut *parser_rc,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -580,8 +642,6 @@ impl<B: DisplayBackend> App<B> {
|
||||
// This callback is triggered in 2 cases:
|
||||
// - When the monitor of this window gets disconnected
|
||||
// - When the window is closed manually.
|
||||
// We don't distinguish here and assume the window should be reopened once a monitor
|
||||
// becomes available again
|
||||
move |auto_reopen| {
|
||||
let (response_sender, _) = daemon_response::create_pair();
|
||||
let command = DaemonCommand::CloseWindows {
|
||||
@@ -599,10 +659,10 @@ impl<B: DisplayBackend> App<B> {
|
||||
|
||||
// handling users close request
|
||||
ewwii_window.delete_event_handler_id =
|
||||
Some(ewwii_window.gtk_window.connect_delete_event({
|
||||
Some(ewwii_window.gtk_window.connect_close_request({
|
||||
let handler = gtk_close_handler.clone();
|
||||
let closed_by_user = closed_by_user.clone();
|
||||
move |_, _| {
|
||||
move |_| {
|
||||
handler(false); // -- false: don't reopen window to respect users intent
|
||||
closed_by_user.set(true);
|
||||
glib::Propagation::Proceed
|
||||
@@ -669,53 +729,134 @@ impl<B: DisplayBackend> App<B> {
|
||||
log::info!("Reloading windows");
|
||||
log::trace!("loading config: {:#?}", config);
|
||||
|
||||
self.ewwii_config = config;
|
||||
self.reloading = true;
|
||||
let result = (|| -> Result<()> {
|
||||
self.ewwii_config.replace_data(config);
|
||||
|
||||
let open_window_ids: Vec<String> = self
|
||||
.open_windows
|
||||
.keys()
|
||||
.cloned()
|
||||
.chain(self.failed_windows.iter().cloned())
|
||||
.dedup()
|
||||
.collect();
|
||||
|
||||
for instance_id in &open_window_ids {
|
||||
let window_arguments = self.instance_id_to_args.get(instance_id).with_context(|| {
|
||||
format!("Cannot reopen window, initial parameters were not saved correctly for {instance_id}")
|
||||
})?;
|
||||
self.open_window(&window_arguments.clone())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})();
|
||||
self.reloading = false;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Load a given CSS string into the gtk css provider
|
||||
pub fn load_css(&mut self, _file_id: usize, css: &str) -> Result<()> {
|
||||
self.css_provider.load_from_data(&css);
|
||||
|
||||
let open_window_ids: Vec<String> = self
|
||||
.open_windows
|
||||
.keys()
|
||||
.cloned()
|
||||
.chain(self.failed_windows.iter().cloned())
|
||||
.dedup()
|
||||
.collect();
|
||||
for instance_id in &open_window_ids {
|
||||
let window_arguments = self.instance_id_to_args.get(instance_id).with_context(|| {
|
||||
format!("Cannot reopen window, initial parameters were not saved correctly for {instance_id}")
|
||||
})?;
|
||||
self.open_window(&window_arguments.clone())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load a given CSS string into the gtk css provider, returning a nicely formatted [`DiagError`] when GTK errors out
|
||||
pub fn load_css(&mut self, file_id: usize, css: &str) -> Result<()> {
|
||||
if let Err(err) = self.css_provider.load_from_data(css.as_bytes()) {
|
||||
static PATTERN: Lazy<regex::Regex> =
|
||||
Lazy::new(|| regex::Regex::new(r"[^:]*:(\d+):(\d+)(.*)$").unwrap());
|
||||
let nice_error_option: Option<_> = (|| {
|
||||
let captures = PATTERN.captures(err.message())?;
|
||||
let line = captures.get(1).unwrap().as_str().parse::<usize>().ok()?;
|
||||
let msg = captures.get(3).unwrap().as_str();
|
||||
let db = error_handling_ctx::FILE_DATABASE.read().ok()?;
|
||||
let line_range = db.line_range(file_id, line - 1).ok()?;
|
||||
let span = Span(line_range.start, line_range.end - 1, file_id);
|
||||
Some(DiagError(gen_diagnostic!(msg, span)))
|
||||
})();
|
||||
match nice_error_option {
|
||||
Some(error) => Err(anyhow!(error)),
|
||||
None => Err(anyhow!("CSS error: {}", err.message())),
|
||||
/// Perform widget control based on the action
|
||||
pub fn perform_widget_control(
|
||||
&mut self,
|
||||
action: crate::opts::WidgetControlAction,
|
||||
) -> Result<()> {
|
||||
match action {
|
||||
crate::opts::WidgetControlAction::Remove { names } => {
|
||||
if let Ok(mut maybe_registry) = self.widget_reg_store.lock() {
|
||||
if let Some(widget_registry) = maybe_registry.as_mut() {
|
||||
for name in names {
|
||||
widget_registry.remove_widget_by_name(&name);
|
||||
}
|
||||
} else {
|
||||
log::error!("Widget registry is empty");
|
||||
}
|
||||
} else {
|
||||
log::error!("Failed to acquire lock on widget registry");
|
||||
}
|
||||
}
|
||||
crate::opts::WidgetControlAction::Create { rhai_codes, parent_name } => {
|
||||
let mut parser = self.config_parser.borrow_mut();
|
||||
for rhai_code in rhai_codes {
|
||||
let widget_node = parser.eval_code_snippet(&rhai_code)?;
|
||||
let wid = rhai_impl::ast::hash_props(widget_node.props().ok_or_else(|| {
|
||||
anyhow::anyhow!("Failed to retreive the properties of this widget.")
|
||||
})?);
|
||||
|
||||
if let Ok(mut maybe_registry) = self.widget_reg_store.lock() {
|
||||
if let Some(widget_registry) = maybe_registry.as_mut() {
|
||||
let pid =
|
||||
widget_registry.get_widget_id_by_name(&parent_name).ok_or_else(
|
||||
|| anyhow::anyhow!("Widget '{}' not found", parent_name),
|
||||
)?;
|
||||
widget_registry.create_widget(&widget_node, wid, pid)?;
|
||||
} else {
|
||||
log::error!("Widget registry is empty");
|
||||
}
|
||||
} else {
|
||||
log::error!("Failed to acquire lock on widget registry");
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::opts::WidgetControlAction::PropertyUpdate {
|
||||
property_and_value,
|
||||
widget_name,
|
||||
} => {
|
||||
if let Ok(mut maybe_registry) = self.widget_reg_store.lock() {
|
||||
if let Some(widget_registry) = maybe_registry.as_mut() {
|
||||
for (key, value) in &property_and_value {
|
||||
widget_registry.update_property_by_name(
|
||||
&widget_name,
|
||||
(key.clone(), value.clone()),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log::error!("Widget registry is empty");
|
||||
}
|
||||
} else {
|
||||
log::error!("Failed to acquire lock on widget registry");
|
||||
}
|
||||
}
|
||||
crate::opts::WidgetControlAction::AddClass { class, widget_name } => {
|
||||
if let Ok(mut maybe_registry) = self.widget_reg_store.lock() {
|
||||
if let Some(widget_registry) = maybe_registry.as_mut() {
|
||||
widget_registry.update_class_of_widget_by_name(&widget_name, &class, false);
|
||||
} else {
|
||||
log::error!("Widget registry is empty");
|
||||
}
|
||||
} else {
|
||||
log::error!("Failed to acquire lock on widget registry");
|
||||
}
|
||||
}
|
||||
crate::opts::WidgetControlAction::RemoveClass { class, widget_name } => {
|
||||
if let Ok(mut maybe_registry) = self.widget_reg_store.lock() {
|
||||
if let Some(widget_registry) = maybe_registry.as_mut() {
|
||||
widget_registry.update_class_of_widget_by_name(&widget_name, &class, true);
|
||||
} else {
|
||||
log::error!("Widget registry is empty");
|
||||
}
|
||||
} else {
|
||||
log::error!("Failed to acquire lock on widget registry");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Trigger a UI update with the given flags.
|
||||
/// Even if there are no flags, the UI will still be updated.
|
||||
pub fn trigger_ui_update_with(
|
||||
&self,
|
||||
&mut self,
|
||||
inject_vars: Option<HashMap<String, String>>,
|
||||
should_preserve_state: bool,
|
||||
lifetime: Option<String>,
|
||||
) -> Result<()> {
|
||||
let compiled_ast = self.ewwii_config.get_owned_compiled_ast();
|
||||
let config_path = self.paths.get_rhai_path();
|
||||
@@ -724,39 +865,41 @@ impl<B: DisplayBackend> App<B> {
|
||||
bail!("The configuration file `{}` does not exist", config_path.display());
|
||||
}
|
||||
|
||||
let mut reeval_parser = ParseConfig::new();
|
||||
let mut reeval_parser = self.config_parser.borrow_mut();
|
||||
let rhai_code = reeval_parser.code_from_file(&config_path)?;
|
||||
|
||||
let mut scope = ParseConfig::initial_poll_listen_scope(&rhai_code)?;
|
||||
|
||||
if let Some(maybe_all_vars) = &self.pl_handler_store {
|
||||
let all_vars = maybe_all_vars.read().unwrap().clone();
|
||||
let all_vars = self.pl_handler_store.read().unwrap().clone();
|
||||
|
||||
for (name, val) in all_vars {
|
||||
scope.set_value(name.clone(), Dynamic::from(val.clone()));
|
||||
}
|
||||
for (name, val) in all_vars {
|
||||
scope.set_value(name.clone(), Dynamic::from(val.clone()));
|
||||
}
|
||||
|
||||
if let Some(vars) = inject_vars {
|
||||
for (name, val) in vars {
|
||||
scope.set_value(name.clone(), Dynamic::from(val.clone()));
|
||||
let name_clone = name.clone();
|
||||
|
||||
// Preserving the new state.
|
||||
// ---
|
||||
// This is esstentially storing the inject variables
|
||||
// in the poll/listen variable store (or the `pl_handler_store` in self)
|
||||
if should_preserve_state {
|
||||
if let Some(maybe_store) = &self.pl_handler_store {
|
||||
maybe_store.write().unwrap().insert(name, val);
|
||||
self.pl_handler_store.write().unwrap().insert(name, val);
|
||||
if let Some(win_name) = &lifetime {
|
||||
self.clear_pl_onclose.insert(win_name.clone(), name_clone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let compiled_ast_ref = compiled_ast.as_ref().map(|rc| rc.borrow());
|
||||
|
||||
let new_root_widget = reeval_parser.eval_code_with(
|
||||
&rhai_code,
|
||||
Some(scope),
|
||||
compiled_ast.as_deref(),
|
||||
compiled_ast_ref.as_deref(),
|
||||
config_path.to_str(),
|
||||
)?;
|
||||
|
||||
@@ -777,14 +920,14 @@ impl<B: DisplayBackend> App<B> {
|
||||
let compiled_ast = self.ewwii_config.get_owned_compiled_ast();
|
||||
let config_path = self.paths.get_rhai_path();
|
||||
|
||||
let mut reeval_parser = ParseConfig::new();
|
||||
let mut reeval_parser = self.config_parser.borrow_mut();
|
||||
let rhai_code = reeval_parser.code_from_file(&config_path)?;
|
||||
|
||||
let mut scope = ParseConfig::initial_poll_listen_scope(&rhai_code)?;
|
||||
|
||||
// unwrap Rc<AST>
|
||||
// unwrap Rc<RefCell<AST>>
|
||||
let ast_ref: &rhai::AST =
|
||||
compiled_ast.as_ref().ok_or_else(|| anyhow!("AST not compiled yet"))?.as_ref();
|
||||
&*compiled_ast.as_ref().ok_or_else(|| anyhow!("AST not compiled yet"))?.borrow();
|
||||
|
||||
for fn_call in calls {
|
||||
reeval_parser.call_rhai_fn(ast_ref, &fn_call, Some(&mut scope))?;
|
||||
@@ -799,6 +942,92 @@ impl<B: DisplayBackend> App<B> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_ewwii_plugin(&mut self, file_path: String) -> Result<()> {
|
||||
if ACTIVE_PLUGIN.get().is_some() {
|
||||
anyhow::bail!("A plugin is already loaded");
|
||||
}
|
||||
|
||||
let lib = unsafe {
|
||||
libloading::Library::new(file_path)
|
||||
.map_err(|e| anyhow!("Failed to load plugin: {}", e))?
|
||||
};
|
||||
|
||||
let (tx, rx): (
|
||||
std::sync::mpsc::Sender<PluginRequest>,
|
||||
std::sync::mpsc::Receiver<PluginRequest>,
|
||||
) = std::sync::mpsc::channel();
|
||||
|
||||
unsafe {
|
||||
// Each plugin exposes: extern "C" fn create_plugin() -> Box<dyn Plugin>
|
||||
let constructor: libloading::Symbol<unsafe extern "C" fn() -> Box<dyn epapi::Plugin>> =
|
||||
lib.get(b"create_plugin")
|
||||
.map_err(|e| anyhow!("Failed to find create_plugin: {}", e))?;
|
||||
|
||||
let plugin = constructor(); // instantiate plugin
|
||||
|
||||
set_active_plugin(lib)?; // keep library alive
|
||||
|
||||
let host = crate::plugin::EwwiiImpl { requestor: tx.clone() };
|
||||
plugin.init(&host); // call init immediately
|
||||
}
|
||||
|
||||
let cp = self.config_parser.clone();
|
||||
let wgs = self.widget_reg_store.clone();
|
||||
|
||||
let handle_request = move |req: PluginRequest| match req {
|
||||
PluginRequest::RhaiEngineAct(func) => {
|
||||
func(&mut cp.borrow_mut().engine);
|
||||
}
|
||||
PluginRequest::RegisterFunc((name, namespace, func)) => match namespace {
|
||||
epapi::rhai_backend::RhaiFnNamespace::Custom(ns) => {
|
||||
let mut module = rhai::Module::new();
|
||||
module.set_native_fn(name, func);
|
||||
cp.borrow_mut().engine.register_static_module(&ns, module.into());
|
||||
}
|
||||
epapi::rhai_backend::RhaiFnNamespace::Global => {
|
||||
cp.borrow_mut().engine.register_fn(name, func);
|
||||
}
|
||||
},
|
||||
PluginRequest::ListWidgetIds(res_tx) => {
|
||||
let wgs_guard = wgs.lock().unwrap();
|
||||
if let Some(wgs_brw) = wgs_guard.as_ref() {
|
||||
let output: Vec<u64> = wgs_brw.widgets.keys().cloned().collect();
|
||||
let _ = res_tx.send(output);
|
||||
}
|
||||
}
|
||||
PluginRequest::WidgetRegistryAct(func) => {
|
||||
let mut wgs_guard = wgs.lock().unwrap();
|
||||
if let Some(ref mut registry) = *wgs_guard {
|
||||
let repr_map: HashMap<u64, &mut gtk4::Widget> = registry
|
||||
.widgets
|
||||
.iter_mut()
|
||||
.map(|(id, entry)| (*id, &mut entry.widget))
|
||||
.collect();
|
||||
|
||||
func(&mut ewwii_plugin_api::widget_backend::WidgetRegistryRepr {
|
||||
widgets: repr_map,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// quick drain
|
||||
while let Ok(req) = rx.try_recv() {
|
||||
handle_request(req);
|
||||
}
|
||||
|
||||
// handling requests that arrive later
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
while let Ok(req) = rx.recv() {
|
||||
handle_request(req);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -847,7 +1076,7 @@ where
|
||||
fn initialize_window<B: DisplayBackend>(
|
||||
window_init: &WindowInitiator,
|
||||
monitor: Monitor,
|
||||
root_widget: gtk::Widget,
|
||||
root_widget: gtk4::Widget,
|
||||
) -> Result<EwwiiWindow> {
|
||||
let monitor_geometry = monitor.geometry();
|
||||
let (actual_window_rect, x, y) = match window_init.geometry {
|
||||
@@ -861,47 +1090,71 @@ fn initialize_window<B: DisplayBackend>(
|
||||
format!("monitor {} is unavailable", window_init.monitor.clone().unwrap())
|
||||
})?;
|
||||
|
||||
window.set_title(&format!("Ewwii - {}", window_init.name));
|
||||
window.set_position(gtk::WindowPosition::None);
|
||||
window.set_gravity(gdk::Gravity::Center);
|
||||
window.set_title(Some(&format!("Ewwii - {}", window_init.name)));
|
||||
// window.set_position(gtk4::WindowPosition::None);
|
||||
// window.set_gravity(gdk::Gravity::Center);
|
||||
|
||||
if let Some(actual_window_rect) = actual_window_rect {
|
||||
window.set_size_request(actual_window_rect.width(), actual_window_rect.height());
|
||||
window.set_default_size(actual_window_rect.width(), actual_window_rect.height());
|
||||
}
|
||||
window.set_decorated(false);
|
||||
window.set_skip_taskbar_hint(true);
|
||||
window.set_skip_pager_hint(true);
|
||||
// window.set_skip_taskbar_hint(true);
|
||||
// window.set_skip_pager_hint(true);
|
||||
|
||||
// run on_screen_changed to set the visual correctly initially.
|
||||
on_screen_changed(&window, None);
|
||||
window.connect_screen_changed(on_screen_changed);
|
||||
// on_screen_changed(&window, None);
|
||||
// window.connect_screen_changed(on_screen_changed);
|
||||
|
||||
window.add(&root_widget);
|
||||
window.set_child(Some(&root_widget));
|
||||
|
||||
window.realize();
|
||||
gtk4::prelude::WidgetExt::realize(&window);
|
||||
|
||||
#[cfg(feature = "x11")]
|
||||
if B::IS_X11 {
|
||||
if let Some(geometry) = window_init.geometry {
|
||||
let _ = apply_window_position(geometry, monitor_geometry, &window);
|
||||
let (conn, _) = x11rb::rust_connection::RustConnection::connect(None)?;
|
||||
let x11_conn = Rc::new(conn);
|
||||
|
||||
let gdk_surface =
|
||||
window.surface().context("Couldn't get gdk window from gtk window")?;
|
||||
|
||||
let win_xid = gdk_surface
|
||||
.downcast_ref::<gdk4_x11::X11Surface>()
|
||||
.context("Failed to get x11 window for gtk window")?
|
||||
.xid() as u32;
|
||||
|
||||
use x11rb::protocol::xproto::*;
|
||||
x11_conn
|
||||
.clone()
|
||||
.change_window_attributes(
|
||||
win_xid,
|
||||
&ChangeWindowAttributesAux::new().event_mask(EventMask::STRUCTURE_NOTIFY),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let _ = apply_window_position(x11_conn.clone(), geometry, monitor_geometry, &window);
|
||||
if window_init.backend_options.x11.window_type
|
||||
!= crate::window::backend_window_options::X11WindowType::Normal
|
||||
{
|
||||
let last_pos = Rc::new(RefCell::new(None));
|
||||
window.connect_configure_event({
|
||||
let last_pos = last_pos.clone();
|
||||
move |window, _| {
|
||||
let gdk_window = window.window().unwrap();
|
||||
let current_origin = gdk_window.origin();
|
||||
let window_clone = window.clone();
|
||||
let conn_clone = x11_conn.clone();
|
||||
|
||||
let mut last = last_pos.borrow_mut();
|
||||
if Some((current_origin.1, current_origin.2)) != *last {
|
||||
*last = Some((current_origin.1, current_origin.2));
|
||||
let _ = apply_window_position(geometry, monitor_geometry, window);
|
||||
use x11rb::connection::Connection;
|
||||
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
loop {
|
||||
if let Ok(event) = conn_clone.poll_for_event() {
|
||||
if let Some(x11rb::protocol::Event::ConfigureNotify(_ev)) = event {
|
||||
let _ = apply_window_position(
|
||||
conn_clone.clone(),
|
||||
geometry,
|
||||
monitor_geometry,
|
||||
&window_clone,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
glib::timeout_future(std::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -909,7 +1162,7 @@ fn initialize_window<B: DisplayBackend>(
|
||||
display_backend::set_xprops(&window, monitor, window_init)?;
|
||||
}
|
||||
|
||||
window.show_all();
|
||||
window.show();
|
||||
|
||||
Ok(EwwiiWindow {
|
||||
name: window_init.name.clone(),
|
||||
@@ -919,35 +1172,25 @@ fn initialize_window<B: DisplayBackend>(
|
||||
})
|
||||
}
|
||||
|
||||
use rhai_impl::parser::ParseConfig;
|
||||
|
||||
async fn generate_new_widgetnode(
|
||||
all_vars: &HashMap<String, String>,
|
||||
code_path: &Path,
|
||||
compiled_ast: Option<&rhai::AST>,
|
||||
parser: Option<&mut ParseConfig>,
|
||||
parser: &mut ParseConfig,
|
||||
) -> Result<WidgetNode> {
|
||||
let mut owned_parser;
|
||||
let reeval_parser: &mut ParseConfig = match parser {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
owned_parser = ParseConfig::new();
|
||||
&mut owned_parser
|
||||
}
|
||||
};
|
||||
let rhai_code = reeval_parser.code_from_file(&code_path)?;
|
||||
if !code_path.exists() {
|
||||
bail!("The configuration file `{}` does not exist", code_path.display());
|
||||
}
|
||||
|
||||
let rhai_code = parser.code_from_file(&code_path)?;
|
||||
|
||||
let mut scope = ParseConfig::initial_poll_listen_scope(&rhai_code)?;
|
||||
for (name, val) in all_vars {
|
||||
scope.set_value(name.clone(), Dynamic::from(val.clone()));
|
||||
}
|
||||
|
||||
if !code_path.exists() {
|
||||
bail!("The configuration file `{}` does not exist", code_path.display());
|
||||
}
|
||||
|
||||
let new_root_widget =
|
||||
reeval_parser.eval_code_with(&rhai_code, Some(scope), compiled_ast, code_path.to_str())?;
|
||||
parser.eval_code_with(&rhai_code, Some(scope), compiled_ast, code_path.to_str())?;
|
||||
|
||||
Ok(new_root_widget)
|
||||
}
|
||||
@@ -964,29 +1207,40 @@ fn get_opt_level_from(n: u8) -> rhai::OptimizationLevel {
|
||||
/// Apply the provided window-positioning rules to the window.
|
||||
#[cfg(feature = "x11")]
|
||||
fn apply_window_position(
|
||||
conn: Rc<x11rb::rust_connection::RustConnection>,
|
||||
mut window_geometry: WindowGeometry,
|
||||
monitor_geometry: gdk::Rectangle,
|
||||
window: &Window,
|
||||
) -> Result<()> {
|
||||
let gdk_window = window.window().context("Failed to get gdk window from gtk window")?;
|
||||
window_geometry.size = crate::window::window_geometry::Coords::from_pixels(window.size());
|
||||
let actual_window_rect = get_window_rectangle(window_geometry, monitor_geometry);
|
||||
use x11rb::connection::Connection;
|
||||
use x11rb::protocol::xproto::{ConfigureWindowAux, ConnectionExt, Window as XWindow};
|
||||
|
||||
let gdk_origin = gdk_window.origin();
|
||||
let gdk_surface = window.surface().context("Failed to get gdk surface from gtk window")?;
|
||||
|
||||
if actual_window_rect.x() != gdk_origin.1 || actual_window_rect.y() != gdk_origin.2 {
|
||||
gdk_window.move_(actual_window_rect.x(), actual_window_rect.y());
|
||||
if let Some(x11_surface) = gdk_surface.downcast_ref::<gdk4_x11::X11Surface>() {
|
||||
window_geometry.size =
|
||||
crate::window::window_geometry::Coords::from_pixels(window.default_size());
|
||||
let actual_window_rect = get_window_rectangle(window_geometry, monitor_geometry);
|
||||
|
||||
let xid = x11_surface.xid();
|
||||
|
||||
let aux = ConfigureWindowAux::new()
|
||||
.x(actual_window_rect.x() as i32)
|
||||
.y(actual_window_rect.y() as i32);
|
||||
|
||||
conn.as_ref().configure_window(xid as XWindow, &aux)?;
|
||||
conn.as_ref().flush()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_screen_changed(window: &Window, _old_screen: Option<&gdk::Screen>) {
|
||||
let visual = gtk::prelude::GtkWindowExt::screen(window).and_then(|screen| {
|
||||
screen.rgba_visual().filter(|_| screen.is_composited()).or_else(|| screen.system_visual())
|
||||
});
|
||||
window.set_visual(visual.as_ref());
|
||||
}
|
||||
// fn on_screen_changed(window: &Window, _old_screen: Option<&gdk::Screen>) {
|
||||
// let visual = window.screen().and_then(|screen| {
|
||||
// screen.rgba_visual().filter(|_| screen.is_composited()).or_else(|| screen.system_visual())
|
||||
// });
|
||||
// window.set_visual(visual.as_ref());
|
||||
// }
|
||||
|
||||
/// Get the monitor geometry of a given monitor, or the default if none is given
|
||||
fn get_gdk_monitor(identifier: Option<MonitorIdentifier>) -> Result<Monitor> {
|
||||
@@ -997,35 +1251,46 @@ fn get_gdk_monitor(identifier: Option<MonitorIdentifier>) -> Result<Monitor> {
|
||||
mon.with_context(|| {
|
||||
let head = format!("Failed to get monitor {}\nThe available monitors are:", ident);
|
||||
let mut body = String::new();
|
||||
for m in 0..display.n_monitors() {
|
||||
if let Some(model) = display.monitor(m).and_then(|x| x.model()) {
|
||||
body.push_str(format!("\n\t[{}] {}", m, model).as_str());
|
||||
let monitors = display.monitors();
|
||||
for i in 0..monitors.n_items() {
|
||||
if let Some(monitor) = monitors.item(i).and_downcast::<gdk::Monitor>() {
|
||||
if let Some(model) = monitor.model() {
|
||||
body.push_str(format!("\n\t[{}] {}", i, model).as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
format!("{}{}", head, body)
|
||||
})?
|
||||
}
|
||||
None => display
|
||||
.primary_monitor()
|
||||
.context("Failed to get primary monitor from GTK. Try explicitly specifying the monitor on your window.")?,
|
||||
None => {
|
||||
let monitors = display.monitors();
|
||||
if monitors.n_items() == 0 {
|
||||
anyhow::bail!("No monitors found on the display");
|
||||
}
|
||||
|
||||
monitors
|
||||
.item(0)
|
||||
.and_downcast::<gdk::Monitor>()
|
||||
.context("Failed to get the primary monitor from the list of monitors")?
|
||||
}
|
||||
};
|
||||
Ok(monitor)
|
||||
}
|
||||
|
||||
/// Get the name of monitor plug for given monitor number
|
||||
/// workaround gdk not providing this information on wayland in regular calls
|
||||
/// gdk_screen_get_monitor_plug_name is deprecated but works fine for that case
|
||||
fn get_monitor_plug_name(display: &gdk::Display, monitor_num: i32) -> Option<&str> {
|
||||
unsafe {
|
||||
use glib::translate::ToGlibPtr;
|
||||
let plug_name_pointer = gdk_sys::gdk_screen_get_monitor_plug_name(
|
||||
display.default_screen().to_glib_none().0,
|
||||
monitor_num,
|
||||
);
|
||||
use std::ffi::CStr;
|
||||
CStr::from_ptr(plug_name_pointer).to_str().ok()
|
||||
}
|
||||
}
|
||||
// /// Get the name of monitor plug for given monitor number
|
||||
// /// workaround gdk not providing this information on wayland in regular calls
|
||||
// /// gdk_screen_get_monitor_plug_name is deprecated but works fine for that case
|
||||
// fn get_monitor_plug_name(display: &gdk::Display, monitor_num: i32) -> Option<&str> {
|
||||
// unsafe {
|
||||
// use glib::translate::ToGlibPtr;
|
||||
// let plug_name_pointer = gdk_sys::gdk_screen_get_monitor_plug_name(
|
||||
// display.default_screen().to_glib_none().0,
|
||||
// monitor_num,
|
||||
// );
|
||||
// use std::ffi::CStr;
|
||||
// CStr::from_ptr(plug_name_pointer).to_str().ok()
|
||||
// }
|
||||
// }
|
||||
|
||||
/// Returns the [Monitor][gdk::Monitor] structure corresponding to the identifer.
|
||||
/// Outside of x11, only [MonitorIdentifier::Numeric] is supported
|
||||
@@ -1033,6 +1298,7 @@ pub fn get_monitor_from_display(
|
||||
display: &gdk::Display,
|
||||
identifier: &MonitorIdentifier,
|
||||
) -> Option<gdk::Monitor> {
|
||||
let monitors = display.monitors();
|
||||
match identifier {
|
||||
MonitorIdentifier::List(list) => {
|
||||
for ident in list {
|
||||
@@ -1042,13 +1308,27 @@ pub fn get_monitor_from_display(
|
||||
}
|
||||
None
|
||||
}
|
||||
MonitorIdentifier::Primary => display.primary_monitor(),
|
||||
MonitorIdentifier::Numeric(num) => display.monitor(*num),
|
||||
MonitorIdentifier::Primary => {
|
||||
if monitors.n_items() > 0 {
|
||||
monitors.item(0).and_downcast::<gdk::Monitor>()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
MonitorIdentifier::Numeric(num) => {
|
||||
if *num < monitors.n_items() as i32 {
|
||||
monitors.item(*num as u32).and_downcast::<gdk::Monitor>()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
MonitorIdentifier::Name(name) => {
|
||||
for m in 0..display.n_monitors() {
|
||||
if let Some(model) = display.monitor(m).and_then(|x| x.model()) {
|
||||
if model == *name || Some(name.as_str()) == get_monitor_plug_name(display, m) {
|
||||
return display.monitor(m);
|
||||
for i in 0..monitors.n_items() {
|
||||
if let Some(monitor) = monitors.item(i).and_downcast::<gdk::Monitor>() {
|
||||
if let Some(model) = monitor.model() {
|
||||
if model == *name {
|
||||
return Some(monitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Module concerned with handling the global application lifecycle of eww.
|
||||
//! Module concerned with handling the global application lifecycle of ewwii.
|
||||
//! Currently, this only means handling application exit by providing a global
|
||||
//! `recv_exit()` function which can be awaited to receive an event in case of application termination.
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::{
|
||||
window::backend_window_options::BackendWindowOptionsDef,
|
||||
};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -15,8 +16,11 @@ use rhai_impl::{ast::WidgetNode, parser::ParseConfig};
|
||||
|
||||
/// Load an [`EwwiiConfig`] from the config dir of the given [`crate::EwwiiPaths`],
|
||||
/// resetting and applying the global YuckFiles object in [`crate::error_handling_ctx`].
|
||||
pub fn read_from_ewwii_paths(eww_paths: &EwwiiPaths) -> Result<EwwiiConfig> {
|
||||
EwwiiConfig::read_from_dir(eww_paths)
|
||||
pub fn read_from_ewwii_paths(
|
||||
eww_paths: &EwwiiPaths,
|
||||
parser: &mut ParseConfig,
|
||||
) -> Result<EwwiiConfig> {
|
||||
EwwiiConfig::read_from_dir(eww_paths, parser)
|
||||
}
|
||||
|
||||
/// Ewwii configuration structure.
|
||||
@@ -24,7 +28,7 @@ pub fn read_from_ewwii_paths(eww_paths: &EwwiiPaths) -> Result<EwwiiConfig> {
|
||||
pub struct EwwiiConfig {
|
||||
windows: HashMap<String, WindowDefinition>,
|
||||
root_node: Option<Rc<WidgetNode>>,
|
||||
compiled_ast: Option<Rc<AST>>,
|
||||
compiled_ast: Option<Rc<RefCell<AST>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -37,15 +41,12 @@ pub struct WindowDefinition {
|
||||
|
||||
impl EwwiiConfig {
|
||||
/// Load an [`EwwiiConfig`] from the config dir of the given [`crate::EwwiiPaths`], reading the main config file.
|
||||
pub fn read_from_dir(eww_paths: &EwwiiPaths) -> Result<Self> {
|
||||
pub fn read_from_dir(eww_paths: &EwwiiPaths, config_parser: &mut ParseConfig) -> Result<Self> {
|
||||
let rhai_path = eww_paths.get_rhai_path();
|
||||
if !rhai_path.exists() {
|
||||
bail!("The configuration file `{}` does not exist", rhai_path.display());
|
||||
}
|
||||
|
||||
// initialize configuration parser
|
||||
let mut config_parser = ParseConfig::new();
|
||||
|
||||
// get code from file
|
||||
let rhai_code = config_parser.code_from_file(&rhai_path)?;
|
||||
|
||||
@@ -87,7 +88,7 @@ impl EwwiiConfig {
|
||||
Ok(EwwiiConfig {
|
||||
windows: window_definitions,
|
||||
root_node: Some(Rc::new(config_tree)),
|
||||
compiled_ast: Some(Rc::new(compiled_ast)),
|
||||
compiled_ast: Some(Rc::new(RefCell::new(compiled_ast))),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -109,7 +110,18 @@ impl EwwiiConfig {
|
||||
self.root_node.clone().ok_or_else(|| anyhow::anyhow!("root_node is missing"))
|
||||
}
|
||||
|
||||
pub fn get_owned_compiled_ast(&self) -> Option<Rc<AST>> {
|
||||
pub fn get_owned_compiled_ast(&self) -> Option<Rc<RefCell<AST>>> {
|
||||
self.compiled_ast.clone()
|
||||
}
|
||||
|
||||
pub fn replace_data(&mut self, new_dat: Self) {
|
||||
if let (Some(old_ast_rc), Some(new_ast_rc)) =
|
||||
(self.compiled_ast.as_ref(), new_dat.compiled_ast.as_ref())
|
||||
{
|
||||
*old_ast_rc.borrow_mut() = new_ast_rc.borrow().clone();
|
||||
}
|
||||
|
||||
self.windows = new_dat.windows;
|
||||
self.root_node = new_dat.root_node;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{widgets::window::Window, window_initiator::WindowInitiator};
|
||||
use crate::window_initiator::WindowInitiator;
|
||||
use gtk4::Window;
|
||||
|
||||
use gtk::gdk;
|
||||
use gtk4::gdk;
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
pub use platform_wayland::WaylandBackend;
|
||||
@@ -32,10 +33,15 @@ impl DisplayBackend for NoBackend {
|
||||
fn initialize_window(
|
||||
_window_init: &WindowInitiator,
|
||||
_monitor: gdk::Rectangle,
|
||||
x: i32,
|
||||
y: i32,
|
||||
_x: i32,
|
||||
_y: i32,
|
||||
) -> Option<Window> {
|
||||
Some(Window::new(gtk::WindowType::Toplevel, x, y))
|
||||
// top level
|
||||
let window = Window::new();
|
||||
|
||||
// window.move_(x, y);
|
||||
|
||||
Some(window)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +51,11 @@ mod platform_wayland {
|
||||
use crate::window::backend_window_options::WlWindowFocusable;
|
||||
use crate::window::window_definition::WindowStacking;
|
||||
use crate::window::window_geometry::AnchorAlignment;
|
||||
use crate::{widgets::window::Window, window_initiator::WindowInitiator};
|
||||
use gtk::gdk;
|
||||
use gtk::prelude::*;
|
||||
use gtk_layer_shell::{KeyboardMode, LayerShell};
|
||||
use crate::window_initiator::WindowInitiator;
|
||||
use gtk4::gdk;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::Window;
|
||||
use gtk4_layer_shell::{KeyboardMode, LayerShell};
|
||||
|
||||
pub struct WaylandBackend;
|
||||
|
||||
@@ -59,17 +66,11 @@ mod platform_wayland {
|
||||
fn initialize_window(
|
||||
window_init: &WindowInitiator,
|
||||
monitor: gdk::Rectangle,
|
||||
x: i32,
|
||||
y: i32,
|
||||
_x: i32,
|
||||
_y: i32,
|
||||
) -> Option<Window> {
|
||||
let window = Window::new(gtk::WindowType::Toplevel, x, y);
|
||||
|
||||
// Sets the keyboard interactivity
|
||||
match window_init.backend_options.wayland.focusable {
|
||||
WlWindowFocusable::None => window.set_keyboard_mode(KeyboardMode::None),
|
||||
WlWindowFocusable::Exclusive => window.set_keyboard_mode(KeyboardMode::Exclusive),
|
||||
WlWindowFocusable::OnDemand => window.set_keyboard_mode(KeyboardMode::OnDemand),
|
||||
}
|
||||
let window = Window::new();
|
||||
// window.move_(x, y);
|
||||
|
||||
window.set_resizable(window_init.resizable);
|
||||
|
||||
@@ -80,7 +81,7 @@ mod platform_wayland {
|
||||
if let Some(ident) = window_init.monitor.clone() {
|
||||
let display = gdk::Display::default().expect("could not get default display");
|
||||
if let Some(monitor) = crate::app::get_monitor_from_display(&display, &ident) {
|
||||
window.set_monitor(&monitor);
|
||||
window.set_monitor(Some(&monitor));
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
@@ -88,16 +89,16 @@ mod platform_wayland {
|
||||
|
||||
// Sets the layer where the layer shell surface will spawn
|
||||
match window_init.stacking {
|
||||
WindowStacking::Foreground => window.set_layer(gtk_layer_shell::Layer::Top),
|
||||
WindowStacking::Foreground => window.set_layer(gtk4_layer_shell::Layer::Top),
|
||||
WindowStacking::Background => {
|
||||
window.set_layer(gtk_layer_shell::Layer::Background)
|
||||
window.set_layer(gtk4_layer_shell::Layer::Background)
|
||||
}
|
||||
WindowStacking::Bottom => window.set_layer(gtk_layer_shell::Layer::Bottom),
|
||||
WindowStacking::Overlay => window.set_layer(gtk_layer_shell::Layer::Overlay),
|
||||
WindowStacking::Bottom => window.set_layer(gtk4_layer_shell::Layer::Bottom),
|
||||
WindowStacking::Overlay => window.set_layer(gtk4_layer_shell::Layer::Overlay),
|
||||
}
|
||||
|
||||
if let Some(namespace) = &window_init.backend_options.wayland.namespace {
|
||||
window.set_namespace(namespace);
|
||||
window.set_namespace(Some(namespace));
|
||||
}
|
||||
|
||||
if let Some(geometry) = window_init.geometry {
|
||||
@@ -118,23 +119,23 @@ mod platform_wayland {
|
||||
AnchorAlignment::END => bottom = true,
|
||||
}
|
||||
|
||||
window.set_anchor(gtk_layer_shell::Edge::Left, left);
|
||||
window.set_anchor(gtk_layer_shell::Edge::Right, right);
|
||||
window.set_anchor(gtk_layer_shell::Edge::Top, top);
|
||||
window.set_anchor(gtk_layer_shell::Edge::Bottom, bottom);
|
||||
window.set_anchor(gtk4_layer_shell::Edge::Left, left);
|
||||
window.set_anchor(gtk4_layer_shell::Edge::Right, right);
|
||||
window.set_anchor(gtk4_layer_shell::Edge::Top, top);
|
||||
window.set_anchor(gtk4_layer_shell::Edge::Bottom, bottom);
|
||||
|
||||
let xoffset = geometry.offset.x.pixels_relative_to(monitor.width());
|
||||
let yoffset = geometry.offset.y.pixels_relative_to(monitor.height());
|
||||
|
||||
if left {
|
||||
window.set_layer_shell_margin(gtk_layer_shell::Edge::Left, xoffset);
|
||||
window.set_margin(gtk4_layer_shell::Edge::Left, xoffset);
|
||||
} else {
|
||||
window.set_layer_shell_margin(gtk_layer_shell::Edge::Right, xoffset);
|
||||
window.set_margin(gtk4_layer_shell::Edge::Right, xoffset);
|
||||
}
|
||||
if bottom {
|
||||
window.set_layer_shell_margin(gtk_layer_shell::Edge::Bottom, yoffset);
|
||||
window.set_margin(gtk4_layer_shell::Edge::Bottom, yoffset);
|
||||
} else {
|
||||
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, yoffset);
|
||||
window.set_margin(gtk4_layer_shell::Edge::Top, yoffset);
|
||||
}
|
||||
// https://github.com/elkowar/eww/issues/296
|
||||
if window_init.backend_options.wayland.exclusive
|
||||
@@ -149,6 +150,13 @@ mod platform_wayland {
|
||||
}
|
||||
}
|
||||
|
||||
// Sets the keyboard interactivity
|
||||
match window_init.backend_options.wayland.focusable {
|
||||
WlWindowFocusable::None => window.set_keyboard_mode(KeyboardMode::None),
|
||||
WlWindowFocusable::Exclusive => window.set_keyboard_mode(KeyboardMode::Exclusive),
|
||||
WlWindowFocusable::OnDemand => window.set_keyboard_mode(KeyboardMode::OnDemand),
|
||||
}
|
||||
|
||||
Some(window)
|
||||
}
|
||||
}
|
||||
@@ -159,11 +167,13 @@ mod platform_x11 {
|
||||
use crate::window::backend_window_options::Side;
|
||||
use crate::window::backend_window_options::X11WindowType;
|
||||
use crate::window::window_definition::WindowStacking;
|
||||
use crate::{widgets::window::Window, window_initiator::WindowInitiator};
|
||||
use crate::window_initiator::WindowInitiator;
|
||||
use anyhow::{Context, Result};
|
||||
use gdk::Monitor;
|
||||
use gtk::gdk;
|
||||
use gtk::{self, prelude::*};
|
||||
use gtk4::gdk;
|
||||
use gtk4::Window;
|
||||
use gtk4::{self, prelude::*};
|
||||
use std::rc::Rc;
|
||||
use x11rb::protocol::xproto::ConnectionExt;
|
||||
|
||||
use x11rb::{
|
||||
@@ -183,23 +193,19 @@ mod platform_x11 {
|
||||
fn initialize_window(
|
||||
window_init: &WindowInitiator,
|
||||
_monitor: gdk::Rectangle,
|
||||
x: i32,
|
||||
y: i32,
|
||||
_x: i32,
|
||||
_y: i32,
|
||||
) -> Option<Window> {
|
||||
let window_type = if window_init.backend_options.x11.wm_ignore {
|
||||
gtk::WindowType::Popup
|
||||
} else {
|
||||
gtk::WindowType::Toplevel
|
||||
};
|
||||
let window = Window::new(window_type, x, y);
|
||||
let window = Window::new();
|
||||
if window_init.backend_options.x11.wm_ignore {
|
||||
// popup
|
||||
window.set_decorated(false);
|
||||
window.set_modal(true);
|
||||
}; // else: normal, toplevel
|
||||
// window.move_(x, y);
|
||||
|
||||
window.set_resizable(window_init.resizable);
|
||||
window.set_keep_above(window_init.stacking == WindowStacking::Foreground);
|
||||
window.set_keep_below(window_init.stacking == WindowStacking::Background);
|
||||
if window_init.backend_options.x11.sticky {
|
||||
window.stick();
|
||||
} else {
|
||||
window.unstick();
|
||||
}
|
||||
|
||||
Some(window)
|
||||
}
|
||||
}
|
||||
@@ -215,9 +221,9 @@ mod platform_x11 {
|
||||
}
|
||||
|
||||
struct X11BackendConnection {
|
||||
conn: RustConnection<DefaultStream>,
|
||||
conn: Rc<RustConnection<DefaultStream>>,
|
||||
root_window: u32,
|
||||
atoms: AtomCollection,
|
||||
atoms: Rc<AtomCollection>,
|
||||
}
|
||||
|
||||
impl X11BackendConnection {
|
||||
@@ -225,7 +231,11 @@ mod platform_x11 {
|
||||
let (conn, screen_num) = RustConnection::connect(None)?;
|
||||
let screen = conn.setup().roots[screen_num].clone();
|
||||
let atoms = AtomCollection::new(&conn)?.reply()?;
|
||||
Ok(X11BackendConnection { conn, root_window: screen.root, atoms })
|
||||
Ok(X11BackendConnection {
|
||||
conn: Rc::new(conn),
|
||||
root_window: screen.root,
|
||||
atoms: Rc::new(atoms),
|
||||
})
|
||||
}
|
||||
|
||||
fn set_xprops_for(
|
||||
@@ -236,11 +246,13 @@ mod platform_x11 {
|
||||
) -> Result<()> {
|
||||
let monitor_rect = monitor.geometry();
|
||||
let scale_factor = monitor.scale_factor() as u32;
|
||||
let gdk_window = window.window().context("Couldn't get gdk window from gtk window")?;
|
||||
let win_id = gdk_window
|
||||
.downcast_ref::<gdkx11::X11Window>()
|
||||
let gdk_surface =
|
||||
window.surface().context("Couldn't get gdk window from gtk window")?;
|
||||
let win_id = gdk_surface
|
||||
.downcast_ref::<gdk4_x11::X11Surface>()
|
||||
.context("Failed to get x11 window for gtk window")?
|
||||
.xid() as u32;
|
||||
|
||||
let strut_def = window_init.backend_options.x11.struts;
|
||||
let root_window_geometry = self.conn.get_geometry(self.root_window)?.reply()?;
|
||||
|
||||
@@ -313,8 +325,63 @@ mod platform_x11 {
|
||||
)?
|
||||
.check()?;
|
||||
|
||||
// apply the stickiness and fg/bg thingy
|
||||
let sticky_clone = window_init.backend_options.x11.sticky.clone();
|
||||
let stacking_clone = window_init.stacking.clone();
|
||||
let root_window = self.root_window;
|
||||
let conn = Rc::clone(&self.conn);
|
||||
let atoms = Rc::clone(&self.atoms);
|
||||
|
||||
window.connect_show(move |_| {
|
||||
if let Err(err) = Self::set_window_states(
|
||||
&*conn,
|
||||
win_id,
|
||||
&*atoms,
|
||||
sticky_clone,
|
||||
stacking_clone,
|
||||
root_window,
|
||||
) {
|
||||
log::error!("Failed to set window state: {}", err);
|
||||
}
|
||||
});
|
||||
|
||||
self.conn.flush().context("Failed to send requests to X server")
|
||||
}
|
||||
|
||||
fn set_window_states(
|
||||
conn: &impl Connection,
|
||||
win: u32,
|
||||
atoms: &AtomCollection,
|
||||
sticky: bool,
|
||||
stacking: WindowStacking,
|
||||
root_window: u32,
|
||||
) -> Result<()> {
|
||||
let mut states = Vec::new();
|
||||
if sticky {
|
||||
states.push(atoms._NET_WM_STATE_STICKY);
|
||||
}
|
||||
match stacking {
|
||||
WindowStacking::Foreground => states.push(atoms._NET_WM_STATE_ABOVE),
|
||||
WindowStacking::Background => states.push(atoms._NET_WM_STATE_BELOW),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
for state in states {
|
||||
let event = ClientMessageEvent {
|
||||
response_type: CLIENT_MESSAGE_EVENT,
|
||||
format: 32,
|
||||
sequence: 0,
|
||||
window: win,
|
||||
type_: atoms._NET_WM_STATE,
|
||||
data: ClientMessageData::from([1, state, 0, 0, 0]),
|
||||
};
|
||||
|
||||
conn.send_event(true, root_window, EventMask::PROPERTY_CHANGE, event)?;
|
||||
}
|
||||
|
||||
conn.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
x11rb::atom_manager! {
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
#![allow(rustdoc::private_intra_doc_links)]
|
||||
|
||||
// getting gtk stuff
|
||||
extern crate gtk;
|
||||
extern crate gtk4;
|
||||
#[cfg(feature = "wayland")]
|
||||
extern crate gtk_layer_shell as gtk_layer_shell;
|
||||
extern crate gtk4_layer_shell as gtk4_layer_shell;
|
||||
|
||||
// imporing dependencies
|
||||
use anyhow::{Context, Result};
|
||||
@@ -48,6 +48,7 @@ mod gen_diagnostic_macro;
|
||||
mod ipc_server;
|
||||
mod opts;
|
||||
mod paths;
|
||||
mod plugin;
|
||||
mod server;
|
||||
mod util;
|
||||
mod widgets;
|
||||
@@ -147,7 +148,7 @@ fn run<B: DisplayBackend>(opts: opts::Opt, ewwii_binary_name: String) -> Result<
|
||||
|
||||
let should_restart = match &opts.action {
|
||||
opts::Action::ShellCompletions { .. } => unreachable!(),
|
||||
opts::Action::Daemon => opts.restart,
|
||||
opts::Action::Daemon { .. } => opts.restart,
|
||||
opts::Action::WithServer(action) => opts.restart && action.can_start_daemon(),
|
||||
opts::Action::ClientOnly(_) => false,
|
||||
};
|
||||
@@ -167,11 +168,11 @@ fn run<B: DisplayBackend>(opts: opts::Opt, ewwii_binary_name: String) -> Result<
|
||||
}
|
||||
|
||||
// make sure that there isn't already a Ewwii daemon running.
|
||||
opts::Action::Daemon if check_server_running(paths.get_ipc_socket_file()) => {
|
||||
opts::Action::Daemon { .. } if check_server_running(paths.get_ipc_socket_file()) => {
|
||||
eprintln!("Ewwii server already running.");
|
||||
true
|
||||
}
|
||||
opts::Action::Daemon => {
|
||||
opts::Action::Daemon { with_plugin } => {
|
||||
log::info!("Initializing Ewwii server. ({})", paths.get_ipc_socket_file().display());
|
||||
let _ = std::fs::remove_file(paths.get_ipc_socket_file());
|
||||
|
||||
@@ -181,8 +182,12 @@ fn run<B: DisplayBackend>(opts: opts::Opt, ewwii_binary_name: String) -> Result<
|
||||
ewwii_binary_name
|
||||
);
|
||||
}
|
||||
let fork_result =
|
||||
server::initialize_server::<B>(paths.clone(), None, !opts.no_daemonize)?;
|
||||
let fork_result = server::initialize_server::<B>(
|
||||
paths.clone(),
|
||||
None,
|
||||
!opts.no_daemonize,
|
||||
with_plugin,
|
||||
)?;
|
||||
opts.no_daemonize || fork_result == ForkResult::Parent
|
||||
}
|
||||
|
||||
@@ -222,7 +227,7 @@ fn run<B: DisplayBackend>(opts: opts::Opt, ewwii_binary_name: String) -> Result<
|
||||
let (command, response_recv) = action.into_daemon_command();
|
||||
// start the daemon and give it the command
|
||||
let fork_result =
|
||||
server::initialize_server::<B>(paths.clone(), Some(command), true)?;
|
||||
server::initialize_server::<B>(paths.clone(), Some(command), true, None)?;
|
||||
let is_parent = fork_result == ForkResult::Parent;
|
||||
if let (Some(recv), true) = (response_recv, is_parent) {
|
||||
listen_for_daemon_response(recv);
|
||||
|
||||
@@ -67,7 +67,10 @@ pub enum Action {
|
||||
|
||||
/// Start the Ewwii daemon.
|
||||
#[command(name = "daemon", alias = "d")]
|
||||
Daemon,
|
||||
Daemon {
|
||||
#[arg(long)]
|
||||
with_plugin: Option<String>,
|
||||
},
|
||||
|
||||
#[command(flatten)]
|
||||
ClientOnly(ActionClientOnly),
|
||||
@@ -184,6 +187,13 @@ pub enum ActionWithServer {
|
||||
// /// Print out the scope graph structure in graphviz dot format.
|
||||
// #[command(name = "graph")]
|
||||
// ShowGraph,
|
||||
/// Control widgets through CLI.
|
||||
#[command(name = "widget-control", alias = "wc")]
|
||||
WidgetControl {
|
||||
#[command(subcommand)]
|
||||
action: WidgetControlAction,
|
||||
},
|
||||
|
||||
/// Update the widgets of a particular window. Poll/Listen variables will be cleared
|
||||
#[command(name = "update", alias = "u")]
|
||||
TriggerUpdateUI {
|
||||
@@ -194,9 +204,13 @@ pub enum ActionWithServer {
|
||||
#[arg(long = "inject", short = 'i', value_parser = parse_inject_var_map)]
|
||||
inject_vars: Option<HashMap<String, String>>,
|
||||
|
||||
/// Preserve the new updates. Only meaningful if used with inject.
|
||||
/// Preserve the new updates.
|
||||
#[arg(long = "preserve", short = 'p')]
|
||||
should_preserve_state: bool,
|
||||
|
||||
/// Tie the variable lifetime to a window lifetime.
|
||||
#[arg(long = "lifetime", short = 'l')]
|
||||
lifetime: Option<String>,
|
||||
},
|
||||
|
||||
/// Call rhai functions. (NOTE: All poll/listen will default to their initial value)
|
||||
@@ -217,6 +231,67 @@ pub enum ActionWithServer {
|
||||
#[arg(long = "sprint", short = 'p')]
|
||||
print: bool,
|
||||
},
|
||||
|
||||
/// Set a plugin (.so) to the ewwii binary
|
||||
#[command(name = "set-plugin")]
|
||||
SetPlugin {
|
||||
/// The .so file to load
|
||||
#[arg(value_parser = absolute_file_path_parser)]
|
||||
file_path: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Subcommands for widget control
|
||||
#[derive(Subcommand, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum WidgetControlAction {
|
||||
/// Remove widget by name
|
||||
Remove {
|
||||
/// Names of the widgets to remove
|
||||
names: Vec<String>,
|
||||
},
|
||||
|
||||
/// Create widgets
|
||||
Create {
|
||||
/// Rhai code to create widgets from
|
||||
rhai_codes: Vec<String>,
|
||||
|
||||
/// Name of the widget to add these widgets as a child to
|
||||
#[arg(long = "parent", short = 'p')]
|
||||
parent_name: String,
|
||||
},
|
||||
|
||||
/// Update properties of a widget by name
|
||||
PropertyUpdate {
|
||||
/// Properties and its value
|
||||
///
|
||||
/// Format: value="val1" widget_name="val2"
|
||||
#[arg(value_parser = parse_inject_var_map)]
|
||||
property_and_value: HashMap<String, String>,
|
||||
|
||||
/// Name of the widget to update the property of
|
||||
#[arg(long = "widget", short = 'w')]
|
||||
widget_name: String,
|
||||
},
|
||||
|
||||
/// Add a class to a widget with given name
|
||||
AddClass {
|
||||
/// The class to add to the widget
|
||||
class: String,
|
||||
|
||||
/// Name of the widget to add class to
|
||||
#[arg(long = "widget", short = 'w')]
|
||||
widget_name: String,
|
||||
},
|
||||
|
||||
/// Remove a class to a widget with given name
|
||||
RemoveClass {
|
||||
/// The class to remove from the widget
|
||||
class: String,
|
||||
|
||||
/// Name of the widget to remove class from
|
||||
#[arg(long = "widget", short = 'w')]
|
||||
widget_name: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Opt {
|
||||
@@ -278,10 +353,17 @@ impl ActionWithServer {
|
||||
self,
|
||||
) -> (app::DaemonCommand, Option<daemon_response::DaemonResponseReceiver>) {
|
||||
let command = match self {
|
||||
ActionWithServer::TriggerUpdateUI { inject_vars, should_preserve_state } => {
|
||||
ActionWithServer::WidgetControl { action } => {
|
||||
return with_response_channel(|sender| app::DaemonCommand::WidgetControl {
|
||||
action,
|
||||
sender,
|
||||
})
|
||||
}
|
||||
ActionWithServer::TriggerUpdateUI { inject_vars, should_preserve_state, lifetime } => {
|
||||
return with_response_channel(|sender| app::DaemonCommand::TriggerUpdateUI {
|
||||
inject_vars,
|
||||
should_preserve_state,
|
||||
lifetime,
|
||||
sender,
|
||||
})
|
||||
}
|
||||
@@ -355,6 +437,12 @@ impl ActionWithServer {
|
||||
sender,
|
||||
})
|
||||
}
|
||||
ActionWithServer::SetPlugin { file_path } => {
|
||||
return with_response_channel(|sender| app::DaemonCommand::SetPlugin {
|
||||
file_path,
|
||||
sender,
|
||||
})
|
||||
}
|
||||
};
|
||||
(command, None)
|
||||
}
|
||||
@@ -431,3 +519,10 @@ fn parse_inject_var_map(s: &str) -> Result<HashMap<String, String>, String> {
|
||||
}
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn absolute_file_path_parser(s: &str) -> Result<String, String> {
|
||||
let p = std::path::Path::new(s);
|
||||
std::fs::canonicalize(p)
|
||||
.map_err(|e| format!("Failed to canonicalize '{}': {}", s, e))
|
||||
.map(|abs_path| abs_path.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
86
crates/ewwii/src/plugin.rs
Normal file
86
crates/ewwii/src/plugin.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use ewwii_plugin_api::{rhai_backend, widget_backend, EwwiiAPI};
|
||||
use rhai::{Array, Dynamic, Engine, EvalAltResult};
|
||||
use std::sync::mpsc::{channel as mpsc_channel, Receiver, Sender};
|
||||
|
||||
pub(crate) struct EwwiiImpl {
|
||||
pub(crate) requestor: Sender<PluginRequest>,
|
||||
}
|
||||
|
||||
impl EwwiiAPI for EwwiiImpl {
|
||||
// General
|
||||
// "PCL = Plugin Controlled Log"
|
||||
fn print(&self, msg: &str) {
|
||||
println!("[PCL] {}", msg);
|
||||
}
|
||||
|
||||
fn log(&self, msg: &str) {
|
||||
log::info!("[PCL] {}", msg);
|
||||
}
|
||||
|
||||
fn warn(&self, msg: &str) {
|
||||
log::warn!("[PCL] {}", msg);
|
||||
}
|
||||
|
||||
fn error(&self, msg: &str) {
|
||||
log::error!("[PCL] {}", msg);
|
||||
}
|
||||
|
||||
// Rhai Manipulation Stuff
|
||||
fn rhai_engine_action(&self, f: Box<dyn FnOnce(&mut Engine) + Send>) -> Result<(), String> {
|
||||
self.requestor
|
||||
.send(PluginRequest::RhaiEngineAct(f))
|
||||
.map_err(|_| "Failed to send request to host".to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_function(
|
||||
&self,
|
||||
name: String,
|
||||
namespace: rhai_backend::RhaiFnNamespace,
|
||||
f: Box<dyn Fn(Array) -> Result<Dynamic, Box<EvalAltResult>> + Send + Sync>,
|
||||
) -> Result<(), String> {
|
||||
let func_info = (name, namespace, f);
|
||||
|
||||
self.requestor
|
||||
.send(PluginRequest::RegisterFunc(func_info))
|
||||
.map_err(|_| "Failed to send request to host".to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Widget Rendering & Logic
|
||||
fn list_widget_ids(&self) -> Result<Vec<u64>, String> {
|
||||
let (tx, rx): (Sender<Vec<u64>>, Receiver<Vec<u64>>) = mpsc_channel();
|
||||
|
||||
self.requestor
|
||||
.send(PluginRequest::ListWidgetIds(tx))
|
||||
.map_err(|_| "Failed to send request to host".to_string())?;
|
||||
|
||||
match rx.recv() {
|
||||
Ok(r) => Ok(r),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn widget_reg_action(
|
||||
&self,
|
||||
f: Box<dyn FnOnce(&mut widget_backend::WidgetRegistryRepr) + Send>,
|
||||
) -> Result<(), String> {
|
||||
self.requestor
|
||||
.send(PluginRequest::WidgetRegistryAct(f))
|
||||
.map_err(|_| "Failed to send request to host".to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum PluginRequest {
|
||||
RhaiEngineAct(Box<dyn FnOnce(&mut Engine) + Send>),
|
||||
RegisterFunc(
|
||||
(
|
||||
String,
|
||||
rhai_backend::RhaiFnNamespace,
|
||||
Box<dyn Fn(Array) -> Result<Dynamic, Box<EvalAltResult>> + Send + Sync>,
|
||||
),
|
||||
),
|
||||
ListWidgetIds(Sender<Vec<u64>>),
|
||||
WidgetRegistryAct(Box<dyn FnOnce(&mut widget_backend::WidgetRegistryRepr) + Send>),
|
||||
}
|
||||
@@ -5,16 +5,16 @@ use crate::{
|
||||
error_handling_ctx, ipc_server, EwwiiPaths,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use gtk4::prelude::{DisplayExt, ListModelExt};
|
||||
use std::{
|
||||
// cell::RefCell,
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
io::Write,
|
||||
marker::PhantomData,
|
||||
os::unix::io::AsRawFd,
|
||||
path::Path,
|
||||
// rc::Rc,
|
||||
sync::{atomic::Ordering, Arc},
|
||||
rc::Rc,
|
||||
sync::{atomic::Ordering, Arc, RwLock},
|
||||
};
|
||||
use tokio::sync::mpsc::*;
|
||||
|
||||
@@ -22,6 +22,7 @@ pub fn initialize_server<B: DisplayBackend>(
|
||||
paths: EwwiiPaths,
|
||||
action: Option<DaemonCommand>,
|
||||
should_daemonize: bool,
|
||||
ewwii_plugin_path: Option<String>,
|
||||
) -> Result<ForkResult> {
|
||||
let (ui_send, mut ui_recv) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
@@ -31,18 +32,11 @@ pub fn initialize_server<B: DisplayBackend>(
|
||||
|
||||
log::info!("Loading paths: {}", &paths);
|
||||
|
||||
let read_config = config::read_from_ewwii_paths(&paths);
|
||||
let pl_handler_store: rhai_impl::updates::ReactiveVarStore =
|
||||
Arc::new(RwLock::new(HashMap::new()));
|
||||
|
||||
let ewwii_config = match read_config {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
error_handling_ctx::print_error(err);
|
||||
config::EwwiiConfig::default()
|
||||
|
||||
// TODO: Maybe do something so that we can exit if user wants.
|
||||
// std::process::exit(1);
|
||||
}
|
||||
};
|
||||
let config_parser =
|
||||
Rc::new(RefCell::new(rhai_impl::parser::ParseConfig::new(Some(pl_handler_store.clone()))));
|
||||
|
||||
cleanup_log_dir(paths.get_log_dir())?;
|
||||
|
||||
@@ -76,29 +70,57 @@ pub fn initialize_server<B: DisplayBackend>(
|
||||
if B::IS_WAYLAND {
|
||||
std::env::set_var("GDK_BACKEND", "wayland")
|
||||
}
|
||||
gtk::init()?;
|
||||
|
||||
gtk4::init()?;
|
||||
|
||||
let main_loop = gtk4::glib::MainLoop::new(None, false);
|
||||
|
||||
let mut app: App<B> = app::App {
|
||||
ewwii_config,
|
||||
ewwii_config: config::EwwiiConfig::default(),
|
||||
open_windows: HashMap::new(),
|
||||
failed_windows: HashSet::new(),
|
||||
instance_id_to_args: HashMap::new(),
|
||||
css_provider: gtk::CssProvider::new(),
|
||||
css_provider: gtk4::CssProvider::new(),
|
||||
reloading: false,
|
||||
app_evt_send: ui_send.clone(),
|
||||
window_close_timer_abort_senders: HashMap::new(),
|
||||
widget_reg_store: std::rc::Rc::new(std::sync::Mutex::new(None)),
|
||||
pl_handler_store: None,
|
||||
pl_handler_store,
|
||||
clear_pl_onclose: HashMap::new(),
|
||||
rt_engine_config: EngineConfValues::default(),
|
||||
config_parser,
|
||||
paths,
|
||||
gtk_main_loop: main_loop.clone(),
|
||||
phantom: PhantomData,
|
||||
};
|
||||
|
||||
if let Some(screen) = gtk::gdk::Screen::default() {
|
||||
gtk::StyleContext::add_provider_for_screen(
|
||||
&screen,
|
||||
&app.css_provider,
|
||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
);
|
||||
// start up plugins
|
||||
if let Some(ewwii_plugin) = ewwii_plugin_path {
|
||||
if let Err(e) = app.set_ewwii_plugin(ewwii_plugin) {
|
||||
error_handling_ctx::print_error(e);
|
||||
}
|
||||
}
|
||||
|
||||
let mut config_parser_mut = app.config_parser.borrow_mut();
|
||||
let read_config = config::read_from_ewwii_paths(&app.paths, &mut *config_parser_mut);
|
||||
|
||||
// free the temporary parser borrow
|
||||
drop(config_parser_mut);
|
||||
|
||||
match read_config {
|
||||
Ok(new_config) => {
|
||||
app.ewwii_config = new_config;
|
||||
}
|
||||
Err(err) => {
|
||||
error_handling_ctx::print_error(err);
|
||||
|
||||
// TODO: Maybe do something so that we can exit if user wants.
|
||||
// std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(display) = gtk4::gdk::Display::default() {
|
||||
gtk4::style_context_add_provider_for_display(&display, &app.css_provider, 900);
|
||||
}
|
||||
|
||||
if let Ok((file_id, css)) = config::scss::parse_scss_from_config(app.paths.get_config_dir()) {
|
||||
@@ -112,7 +134,7 @@ pub fn initialize_server<B: DisplayBackend>(
|
||||
// initialize all the handlers and tasks running asyncronously
|
||||
let tokio_handle = init_async_part(app.paths.clone(), ui_send);
|
||||
|
||||
gtk::glib::MainContext::default().spawn_local(async move {
|
||||
gtk4::glib::MainContext::default().spawn_local(async move {
|
||||
// if an action was given to the daemon initially, execute it first.
|
||||
if let Some(action) = action {
|
||||
app.handle_command(action).await;
|
||||
@@ -134,20 +156,26 @@ pub fn initialize_server<B: DisplayBackend>(
|
||||
// allow the GTK main thread to do tokio things
|
||||
let _g = tokio_handle.enter();
|
||||
|
||||
gtk::main();
|
||||
main_loop.run();
|
||||
log::info!("main application thread finished");
|
||||
|
||||
Ok(ForkResult::Child)
|
||||
}
|
||||
|
||||
fn connect_monitor_added(ui_send: UnboundedSender<DaemonCommand>) {
|
||||
let display = gtk::gdk::Display::default().expect("could not get default display");
|
||||
display.connect_monitor_added({
|
||||
move |_display: >k::gdk::Display, _monitor: >k::gdk::Monitor| {
|
||||
log::info!("New monitor connected, reloading configuration");
|
||||
let _ = reload_config_and_css(&ui_send);
|
||||
}
|
||||
});
|
||||
if let Some(display) = gtk4::gdk::Display::default() {
|
||||
let monitors = display.monitors();
|
||||
|
||||
monitors.connect_items_changed(gtk4::glib::clone!(
|
||||
#[strong]
|
||||
ui_send,
|
||||
move |_, _, _, _| {
|
||||
let _ = reload_config_and_css(&ui_send);
|
||||
}
|
||||
));
|
||||
} else {
|
||||
log::warn!("Cannot access GDK Display on this session (likely Wayland)");
|
||||
}
|
||||
}
|
||||
|
||||
fn reload_config_and_css(ui_send: &UnboundedSender<DaemonCommand>) -> Result<()> {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use anyhow::Result;
|
||||
use gtk::gdk::prelude::Cast;
|
||||
use gtk4::gdk::prelude::Cast;
|
||||
|
||||
use crate::{config::WindowDefinition, widgets::widget_definitions::*};
|
||||
use crate::config::WindowDefinition;
|
||||
use crate::widgets::widget_definitions::*;
|
||||
|
||||
use rhai_impl::ast::WidgetNode;
|
||||
|
||||
@@ -17,7 +18,7 @@ pub enum WidgetInput<'a> {
|
||||
pub fn build_gtk_widget<'a>(
|
||||
input: &'a WidgetInput<'a>,
|
||||
widget_reg: &mut WidgetRegistry,
|
||||
) -> Result<gtk::Widget> {
|
||||
) -> Result<gtk4::Widget> {
|
||||
let node: &'a WidgetNode = match input {
|
||||
WidgetInput::Node(n) => n,
|
||||
WidgetInput::BorrowedNode(n) => n,
|
||||
@@ -30,7 +31,7 @@ pub fn build_gtk_widget<'a>(
|
||||
fn build_gtk_widget_from_node(
|
||||
root_node: &WidgetNode,
|
||||
widget_reg: &mut WidgetRegistry,
|
||||
) -> Result<gtk::Widget> {
|
||||
) -> Result<gtk4::Widget> {
|
||||
/*
|
||||
When a a new widget is added to the build process,
|
||||
make sure to update get_id_to_props_map() found in
|
||||
@@ -40,23 +41,30 @@ fn build_gtk_widget_from_node(
|
||||
|
||||
let gtk_widget = match root_node {
|
||||
WidgetNode::Box { props, children } => build_gtk_box(props, children, widget_reg)?.upcast(),
|
||||
WidgetNode::CenterBox { props, children } => {
|
||||
build_center_box(props, children, widget_reg)?.upcast()
|
||||
WidgetNode::FlowBox { props, children } => {
|
||||
build_gtk_flowbox(props, children, widget_reg)?.upcast()
|
||||
}
|
||||
WidgetNode::EventBox { props, children } => {
|
||||
build_gtk_event_box(props, children, widget_reg)?.upcast()
|
||||
build_event_box(props, children, widget_reg)?.upcast()
|
||||
}
|
||||
WidgetNode::ToolTip { props, children } => {
|
||||
build_tooltip(props, children, widget_reg)?.upcast()
|
||||
}
|
||||
WidgetNode::LocalBind { props, children } => {
|
||||
build_localbind_util(props, children, widget_reg)?.upcast()
|
||||
}
|
||||
WidgetNode::WidgetAction { props, children } => {
|
||||
build_widgetaction_util(props, children, widget_reg)?.upcast()
|
||||
}
|
||||
WidgetNode::CircularProgress { props } => {
|
||||
build_circular_progress_bar(props, widget_reg)?.upcast()
|
||||
}
|
||||
WidgetNode::GtkUI { props } => build_gtk_ui_file(props)?.upcast(),
|
||||
WidgetNode::Graph { props } => build_graph(props, widget_reg)?.upcast(),
|
||||
WidgetNode::Transform { props } => build_transform(props, widget_reg)?.upcast(),
|
||||
WidgetNode::Slider { props } => build_gtk_scale(props, widget_reg)?.upcast(),
|
||||
// WidgetNode::Transform { props } => build_transform(props, widget_reg)?.upcast(),
|
||||
WidgetNode::Scale { props } => build_gtk_scale(props, widget_reg)?.upcast(),
|
||||
WidgetNode::Progress { props } => build_gtk_progress(props, widget_reg)?.upcast(),
|
||||
WidgetNode::Image { props } => build_gtk_image(props, widget_reg)?.upcast(),
|
||||
WidgetNode::Image { props } => build_image(props, widget_reg)?.upcast(),
|
||||
WidgetNode::Button { props } => build_gtk_button(props, widget_reg)?.upcast(),
|
||||
WidgetNode::Label { props } => build_gtk_label(props, widget_reg)?.upcast(),
|
||||
// WIDGET_NAME_LITERAL => build_gtk_literal(node)?.upcast(),
|
||||
|
||||
@@ -1,206 +1,124 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use gtk::glib::{self, object_subclass, prelude::*, wrapper, Properties};
|
||||
use gtk::{cairo, gdk, prelude::*, subclass::prelude::*};
|
||||
use std::cell::RefCell;
|
||||
use glib::Object;
|
||||
use gtk4::glib;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::subclass::prelude::*;
|
||||
use gtk4::{cairo, gdk, graphene};
|
||||
use std::cell::Cell;
|
||||
|
||||
use crate::error_handling_ctx;
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
wrapper! {
|
||||
pub struct CircProg(ObjectSubclass<CircProgPriv>)
|
||||
@extends gtk::Bin, gtk::Container, gtk::Widget;
|
||||
}
|
||||
|
||||
#[derive(Properties)]
|
||||
#[properties(wrapper_type = CircProg)]
|
||||
pub struct CircProgPriv {
|
||||
#[property(
|
||||
get,
|
||||
set,
|
||||
nick = "Starting at",
|
||||
blurb = "Starting at",
|
||||
minimum = 0f64,
|
||||
maximum = 100f64,
|
||||
default = 0f64
|
||||
)]
|
||||
start_at: RefCell<f64>,
|
||||
|
||||
#[property(
|
||||
get,
|
||||
set,
|
||||
nick = "Value",
|
||||
blurb = "The value",
|
||||
minimum = 0f64,
|
||||
maximum = 100f64,
|
||||
default = 0f64
|
||||
)]
|
||||
value: RefCell<f64>,
|
||||
|
||||
#[property(
|
||||
get,
|
||||
set,
|
||||
nick = "Thickness",
|
||||
blurb = "Thickness",
|
||||
minimum = 0f64,
|
||||
maximum = 100f64,
|
||||
default = 1f64
|
||||
)]
|
||||
thickness: RefCell<f64>,
|
||||
|
||||
#[property(get, set, nick = "Clockwise", blurb = "Clockwise", default = true)]
|
||||
clockwise: RefCell<bool>,
|
||||
|
||||
content: RefCell<Option<gtk::Widget>>,
|
||||
}
|
||||
|
||||
// This should match the default values from the ParamSpecs
|
||||
impl Default for CircProgPriv {
|
||||
fn default() -> Self {
|
||||
CircProgPriv {
|
||||
start_at: RefCell::new(0.0),
|
||||
value: RefCell::new(0.0),
|
||||
thickness: RefCell::new(1.0),
|
||||
clockwise: RefCell::new(true),
|
||||
content: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for CircProgPriv {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
Self::derived_properties()
|
||||
pub struct CircProg {
|
||||
pub value: Cell<f64>,
|
||||
pub start_at: Cell<f64>,
|
||||
pub thickness: Cell<f64>,
|
||||
pub clockwise: Cell<bool>,
|
||||
pub fg_color: Cell<gdk::RGBA>,
|
||||
pub bg_color: Cell<gdk::RGBA>,
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"value" => {
|
||||
self.value.replace(value.get().unwrap());
|
||||
self.obj().queue_draw(); // Queue a draw call with the updated value
|
||||
impl Default for CircProg {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
value: Cell::new(0.0),
|
||||
start_at: Cell::new(0.0),
|
||||
thickness: Cell::new(8.0),
|
||||
clockwise: Cell::new(true),
|
||||
fg_color: Cell::new(gdk::RGBA::new(1.0, 0.0, 0.0, 1.0)),
|
||||
bg_color: Cell::new(gdk::RGBA::new(0.0, 0.0, 0.0, 0.1)),
|
||||
}
|
||||
"thickness" => {
|
||||
self.thickness.replace(value.get().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for CircProg {
|
||||
const NAME: &'static str = "CircProg";
|
||||
type Type = super::CircProg;
|
||||
type ParentType = gtk4::Widget;
|
||||
}
|
||||
|
||||
impl ObjectImpl for CircProg {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
let obj = self.obj();
|
||||
obj.add_css_class("circular-progress");
|
||||
}
|
||||
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
use once_cell::sync::Lazy;
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecDouble::builder("value")
|
||||
.minimum(0.0)
|
||||
.maximum(100.0)
|
||||
.default_value(0.0)
|
||||
.build(),
|
||||
glib::ParamSpecDouble::builder("start-at")
|
||||
.minimum(0.0)
|
||||
.maximum(100.0)
|
||||
.default_value(0.0)
|
||||
.build(),
|
||||
glib::ParamSpecDouble::builder("thickness")
|
||||
.minimum(1.0)
|
||||
.maximum(50.0)
|
||||
.default_value(8.0)
|
||||
.build(),
|
||||
glib::ParamSpecBoolean::builder("clockwise").default_value(true).build(),
|
||||
glib::ParamSpecBoxed::builder::<gdk::RGBA>("fg-color").build(),
|
||||
glib::ParamSpecBoxed::builder::<gdk::RGBA>("bg-color").build(),
|
||||
]
|
||||
});
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"value" => self.value.set(value.get().unwrap()),
|
||||
"start-at" => self.start_at.set(value.get().unwrap()),
|
||||
"thickness" => self.thickness.set(value.get().unwrap()),
|
||||
"clockwise" => self.clockwise.set(value.get().unwrap()),
|
||||
"fg-color" => self.fg_color.set(value.get().unwrap()),
|
||||
"bg-color" => self.bg_color.set(value.get().unwrap()),
|
||||
x => panic!("Tried to set inexistant property of CircProg: {}", x,),
|
||||
}
|
||||
"start-at" => {
|
||||
self.start_at.replace(value.get().unwrap());
|
||||
self.obj().queue_draw();
|
||||
}
|
||||
|
||||
fn property(&self, _: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"value" => self.value.get().to_value(),
|
||||
"start-at" => self.start_at.get().to_value(),
|
||||
"thickness" => self.thickness.get().to_value(),
|
||||
"clockwise" => self.clockwise.get().to_value(),
|
||||
"fg-color" => self.fg_color.get().to_value(),
|
||||
"bg-color" => self.bg_color.get().to_value(),
|
||||
x => panic!("Tried to get inexistant property of CircProg: {}", x,),
|
||||
}
|
||||
"clockwise" => {
|
||||
self.clockwise.replace(value.get().unwrap());
|
||||
}
|
||||
x => panic!("Tried to set inexistant property of CircProg: {}", x,),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
self.derived_property(id, pspec)
|
||||
}
|
||||
}
|
||||
|
||||
#[object_subclass]
|
||||
impl ObjectSubclass for CircProgPriv {
|
||||
type ParentType = gtk::Bin;
|
||||
type Type = CircProg;
|
||||
|
||||
const NAME: &'static str = "CircProg";
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.set_css_name("circular-progress");
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CircProg {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CircProg {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new::<Self>()
|
||||
}
|
||||
}
|
||||
|
||||
impl ContainerImpl for CircProgPriv {
|
||||
fn add(&self, widget: >k::Widget) {
|
||||
if let Some(content) = &*self.content.borrow() {
|
||||
// TODO: Handle this error when populating children widgets instead
|
||||
error_handling_ctx::print_error(anyhow!(
|
||||
"Error, trying to add multiple children to a circular-progress widget"
|
||||
));
|
||||
self.parent_remove(content);
|
||||
impl WidgetImpl for CircProg {
|
||||
fn measure(&self, _orientation: gtk4::Orientation, _for_size: i32) -> (i32, i32, i32, i32) {
|
||||
let min_size = 32;
|
||||
let natural_size = 64;
|
||||
(min_size, natural_size, -1, -1)
|
||||
}
|
||||
self.parent_add(widget);
|
||||
self.content.replace(Some(widget.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
fn calc_widget_lowest_preferred_dimension(widget: >k::Widget) -> (i32, i32) {
|
||||
let preferred_width = widget.preferred_width();
|
||||
let preferred_height = widget.preferred_height();
|
||||
let min_lowest = i32::min(preferred_width.0, preferred_height.0);
|
||||
let natural_lowest = i32::min(preferred_width.1, preferred_height.1);
|
||||
(min_lowest, natural_lowest)
|
||||
}
|
||||
fn snapshot(&self, snapshot: >k4::Snapshot) {
|
||||
let value = self.value.get();
|
||||
let start_at = self.start_at.get();
|
||||
let thickness = self.thickness.get();
|
||||
let clockwise = self.clockwise.get();
|
||||
let fg_color = self.fg_color.get();
|
||||
let bg_color = self.bg_color.get();
|
||||
|
||||
impl BinImpl for CircProgPriv {}
|
||||
|
||||
impl WidgetImpl for CircProgPriv {
|
||||
// We overwrite preferred_* so that overflowing content from the children gets cropped
|
||||
// We return min(child_width, child_height)
|
||||
fn preferred_width(&self) -> (i32, i32) {
|
||||
let styles = self.obj().style_context();
|
||||
let margin = styles.margin(gtk::StateFlags::NORMAL);
|
||||
|
||||
if let Some(child) = &*self.content.borrow() {
|
||||
let (min_child, natural_child) = calc_widget_lowest_preferred_dimension(child);
|
||||
(
|
||||
min_child + margin.right as i32 + margin.left as i32,
|
||||
natural_child + margin.right as i32 + margin.left as i32,
|
||||
)
|
||||
} else {
|
||||
let empty_width =
|
||||
(2 * *self.thickness.borrow() as i32) + margin.right as i32 + margin.left as i32;
|
||||
(empty_width, empty_width)
|
||||
}
|
||||
}
|
||||
|
||||
fn preferred_width_for_height(&self, _height: i32) -> (i32, i32) {
|
||||
self.preferred_width()
|
||||
}
|
||||
|
||||
fn preferred_height(&self) -> (i32, i32) {
|
||||
let styles = self.obj().style_context();
|
||||
let margin = styles.margin(gtk::StateFlags::NORMAL);
|
||||
|
||||
if let Some(child) = &*self.content.borrow() {
|
||||
let (min_child, natural_child) = calc_widget_lowest_preferred_dimension(child);
|
||||
(
|
||||
min_child + margin.bottom as i32 + margin.top as i32,
|
||||
natural_child + margin.bottom as i32 + margin.top as i32,
|
||||
)
|
||||
} else {
|
||||
let empty_height =
|
||||
(2 * *self.thickness.borrow() as i32) + margin.right as i32 + margin.left as i32;
|
||||
(empty_height, empty_height)
|
||||
}
|
||||
}
|
||||
|
||||
fn preferred_height_for_width(&self, _width: i32) -> (i32, i32) {
|
||||
self.preferred_height()
|
||||
}
|
||||
|
||||
fn draw(&self, cr: &cairo::Context) -> glib::Propagation {
|
||||
let res: Result<()> = (|| {
|
||||
let value = *self.value.borrow();
|
||||
let start_at = *self.start_at.borrow();
|
||||
let thickness = *self.thickness.borrow();
|
||||
let clockwise = *self.clockwise.borrow();
|
||||
|
||||
let styles = self.obj().style_context();
|
||||
let margin = styles.margin(gtk::StateFlags::NORMAL);
|
||||
let margin_start = self.obj().margin_start() as f64;
|
||||
let margin_end = self.obj().margin_end() as f64;
|
||||
let margin_top = self.obj().margin_top() as f64;
|
||||
let margin_bottom = self.obj().margin_bottom() as f64;
|
||||
// Padding is not supported yet
|
||||
let fg_color: gdk::RGBA = styles.color(gtk::StateFlags::NORMAL);
|
||||
let bg_color: gdk::RGBA = styles
|
||||
.style_property_for_state("background-color", gtk::StateFlags::NORMAL)
|
||||
.get()?;
|
||||
|
||||
let (start_angle, end_angle) = if clockwise {
|
||||
(0.0, perc_to_rad(value))
|
||||
} else {
|
||||
@@ -211,12 +129,20 @@ impl WidgetImpl for CircProgPriv {
|
||||
let total_height = self.obj().allocated_height() as f64;
|
||||
let center = (total_width / 2.0, total_height / 2.0);
|
||||
|
||||
let circle_width = total_width - margin.left as f64 - margin.right as f64;
|
||||
let circle_height = total_height - margin.top as f64 - margin.bottom as f64;
|
||||
let circle_width = total_width - margin_start - margin_end;
|
||||
let circle_height = total_height - margin_top - margin_bottom;
|
||||
let outer_ring = f64::min(circle_width, circle_height) / 2.0;
|
||||
let inner_ring = (f64::min(circle_width, circle_height) / 2.0) - thickness;
|
||||
|
||||
cr.save()?;
|
||||
// Snapshot Cairo node
|
||||
let cr = snapshot.append_cairo(&graphene::Rect::new(
|
||||
0.0_f32,
|
||||
0.0_f32,
|
||||
total_width as f32,
|
||||
total_height as f32,
|
||||
));
|
||||
|
||||
cr.save().unwrap();
|
||||
|
||||
// Centering
|
||||
cr.translate(center.0, center.1);
|
||||
@@ -226,45 +152,45 @@ impl WidgetImpl for CircProgPriv {
|
||||
// Background Ring
|
||||
cr.move_to(center.0, center.1);
|
||||
cr.arc(center.0, center.1, outer_ring, 0.0, perc_to_rad(100.0));
|
||||
cr.set_source_rgba(bg_color.red(), bg_color.green(), bg_color.blue(), bg_color.alpha());
|
||||
cr.set_source_rgba(
|
||||
bg_color.red().into(),
|
||||
bg_color.green().into(),
|
||||
bg_color.blue().into(),
|
||||
bg_color.alpha().into(),
|
||||
);
|
||||
cr.move_to(center.0, center.1);
|
||||
cr.arc(center.0, center.1, inner_ring, 0.0, perc_to_rad(100.0));
|
||||
cr.set_fill_rule(cairo::FillRule::EvenOdd); // Substract one circle from the other
|
||||
cr.fill()?;
|
||||
cr.fill().unwrap();
|
||||
|
||||
// Foreground Ring
|
||||
cr.move_to(center.0, center.1);
|
||||
cr.arc(center.0, center.1, outer_ring, start_angle, end_angle);
|
||||
cr.set_source_rgba(fg_color.red(), fg_color.green(), fg_color.blue(), fg_color.alpha());
|
||||
cr.set_source_rgba(
|
||||
fg_color.red().into(),
|
||||
fg_color.green().into(),
|
||||
fg_color.blue().into(),
|
||||
fg_color.alpha().into(),
|
||||
);
|
||||
cr.move_to(center.0, center.1);
|
||||
cr.arc(center.0, center.1, inner_ring, start_angle, end_angle);
|
||||
cr.set_fill_rule(cairo::FillRule::EvenOdd); // Substract one circle from the other
|
||||
cr.fill()?;
|
||||
cr.restore()?;
|
||||
cr.fill().unwrap();
|
||||
|
||||
// Draw the children widget, clipping it to the inside
|
||||
if let Some(child) = &*self.content.borrow() {
|
||||
cr.save()?;
|
||||
cr.restore().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Center circular clip
|
||||
cr.arc(center.0, center.1, inner_ring + 1.0, 0.0, perc_to_rad(100.0));
|
||||
cr.set_source_rgba(bg_color.red(), 0.0, 0.0, bg_color.alpha());
|
||||
cr.clip();
|
||||
glib::wrapper! {
|
||||
pub struct CircProg(ObjectSubclass<imp::CircProg>)
|
||||
@extends gtk4::Widget,
|
||||
@implements gtk4::Accessible, gtk4::Actionable, gtk4::Buildable, gtk4::ConstraintTarget;
|
||||
}
|
||||
|
||||
// Children widget
|
||||
self.obj().propagate_draw(child, cr);
|
||||
|
||||
cr.reset_clip();
|
||||
cr.restore()?;
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
if let Err(error) = res {
|
||||
error_handling_ctx::print_error(error)
|
||||
};
|
||||
|
||||
glib::Propagation::Proceed
|
||||
impl CircProg {
|
||||
pub fn new() -> Self {
|
||||
Object::builder().build()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,349 +1,654 @@
|
||||
use std::{cell::RefCell, collections::VecDeque};
|
||||
// https://www.figuiere.net/technotes/notes/tn002/
|
||||
// https://github.com/gtk-rs/examples/blob/master/src/bin/listbox_model.rs
|
||||
use anyhow::{anyhow, Result};
|
||||
use gtk::glib::{self, object_subclass, wrapper, Properties};
|
||||
use gtk::{cairo, gdk, prelude::*, subclass::prelude::*};
|
||||
//! Graph widget with time-series visualization
|
||||
//!
|
||||
//! The graph renders data points in a 2D canvas with time on the horizontal axis
|
||||
//! and values on the vertical axis by default (non-vertical mode).
|
||||
//!
|
||||
//! Canvas Layout (default horizontal orientation, no flipping):
|
||||
//!
|
||||
//! value ↑
|
||||
//! │
|
||||
//! max ┼──────────────┐ (past, max)
|
||||
//! │ │
|
||||
//! │ │
|
||||
//! │ Graph │
|
||||
//! │ Area │
|
||||
//! │ │
|
||||
//! min ┼──────────────┘ (past, min)
|
||||
//! └──────────────┴───→ time
|
||||
//! (past) (now)
|
||||
//!
|
||||
//! Key coordinates in widget space (after margins):
|
||||
//! - (0, 0) : Top-left corner (past time, max value)
|
||||
//! - (width, 0) : Top-right corner (current time, max value)
|
||||
//! - (0, height): Bottom-left corner (past time, min value)
|
||||
//! - (width, height): Bottom-right corner (current time, min value)
|
||||
//!
|
||||
//! Time flows from left (past) to right (present).
|
||||
//! Most recent data points appear at the right edge.
|
||||
//! Older points scroll leftward as time progresses.
|
||||
//!
|
||||
//! The `time-range` property controls how many milliseconds of history are visible.
|
||||
//! Points older than `now - time-range` are automatically pruned.
|
||||
//!
|
||||
//! The graph supports multiple render types (Line, Fill, Step variants)
|
||||
//! and can be flipped/rotated via properties.
|
||||
|
||||
use crate::error_handling_ctx;
|
||||
use gtk4::glib::property::PropertySet;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::subclass::prelude::*;
|
||||
use gtk4::{gdk, glib, graphene, gsk};
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::VecDeque;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
// This widget shouldn't be a Bin/Container but I've not been
|
||||
// able to subclass just a gtk::Widget
|
||||
wrapper! {
|
||||
pub struct Graph(ObjectSubclass<GraphPriv>)
|
||||
@extends gtk::Bin, gtk::Container, gtk::Widget;
|
||||
}
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
#[derive(Properties)]
|
||||
#[properties(wrapper_type = Graph)]
|
||||
pub struct GraphPriv {
|
||||
#[property(get, set, nick = "Value", blurb = "The value", minimum = 0f64, maximum = f64::MAX, default = 0f64)]
|
||||
value: RefCell<f64>,
|
||||
const DEFAULT_VALUE: f64 = 0.0;
|
||||
const DEFAULT_THICKNESS: f64 = 1.0;
|
||||
const DEFAULT_MIN: f64 = 0.0;
|
||||
const DEFAULT_MAX: f64 = 1.0;
|
||||
const DEFAULT_DYNAMIC: bool = true;
|
||||
const DEFAULT_TIME_RANGE: u32 = 10_000; // ms
|
||||
const DEFAULT_FLIP_X: bool = false;
|
||||
const DEFAULT_FLIP_Y: bool = false;
|
||||
const DEFAULT_VERTICAL: bool = false;
|
||||
const DEFAULT_ANIMATE: bool = true;
|
||||
|
||||
#[property(get, set, nick = "Thickness", blurb = "The Thickness", minimum = 0f64, maximum = f64::MAX, default = 1f64)]
|
||||
thickness: RefCell<f64>,
|
||||
pub struct Graph {
|
||||
pub value: Cell<f64>,
|
||||
pub thickness: Cell<f64>,
|
||||
pub line_style: Cell<LineStyle>,
|
||||
pub min: Cell<f64>,
|
||||
pub max: Cell<f64>,
|
||||
pub dynamic: Cell<bool>,
|
||||
pub time_range: Cell<u32>,
|
||||
pub flip_x: Cell<bool>,
|
||||
pub flip_y: Cell<bool>,
|
||||
pub vertical: Cell<bool>,
|
||||
pub render_type: Cell<RenderType>,
|
||||
|
||||
#[property(get, set, nick = "Line Style", blurb = "The Line Style", default = "miter")]
|
||||
line_style: RefCell<String>,
|
||||
// Runtime state
|
||||
history: RefCell<VecDeque<(Instant, f64)>>,
|
||||
last_updated_at: RefCell<Instant>,
|
||||
tick_id: RefCell<Option<gtk4::TickCallbackId>>,
|
||||
min_value_cached: Cell<Option<f64>>,
|
||||
max_value_cached: Cell<Option<f64>>,
|
||||
has_received_value: Cell<bool>,
|
||||
|
||||
#[property(get, set, nick = "Maximum Value", blurb = "The Maximum Value", minimum = 0f64, maximum = f64::MAX, default = 100f64)]
|
||||
min: RefCell<f64>,
|
||||
// Cached path (Geometry)
|
||||
cached_path: RefCell<Option<gsk::Path>>,
|
||||
// The "anchor" time used to build the path (fixed time reference)
|
||||
path_anchor_time: Cell<Instant>,
|
||||
// Size when the path was built (for invalidation)
|
||||
cached_path_size: Cell<(f32, f32)>,
|
||||
}
|
||||
|
||||
#[property(get, set, nick = "Minumum Value", blurb = "The Minimum Value", minimum = 0f64, maximum = f64::MAX, default = 0f64)]
|
||||
max: RefCell<f64>,
|
||||
impl Default for Graph {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
value: Cell::new(DEFAULT_VALUE),
|
||||
thickness: Cell::new(DEFAULT_THICKNESS),
|
||||
line_style: Cell::new(LineStyle::default()),
|
||||
min: Cell::new(DEFAULT_MIN),
|
||||
max: Cell::new(DEFAULT_MAX),
|
||||
dynamic: Cell::new(DEFAULT_DYNAMIC),
|
||||
time_range: Cell::new(DEFAULT_TIME_RANGE),
|
||||
flip_x: Cell::new(DEFAULT_FLIP_X),
|
||||
flip_y: Cell::new(DEFAULT_FLIP_Y),
|
||||
vertical: Cell::new(DEFAULT_VERTICAL),
|
||||
render_type: Cell::new(RenderType::default()),
|
||||
|
||||
#[property(get, set, nick = "Dynamic", blurb = "If it is dynamic", default = true)]
|
||||
dynamic: RefCell<bool>,
|
||||
history: RefCell::new(VecDeque::new()),
|
||||
last_updated_at: RefCell::new(Instant::now()),
|
||||
tick_id: RefCell::new(None),
|
||||
min_value_cached: Cell::new(None),
|
||||
max_value_cached: Cell::new(None),
|
||||
has_received_value: Cell::new(false),
|
||||
|
||||
#[property(get, set, nick = "Time Range", blurb = "The Time Range", minimum = 0u64, maximum = u64::MAX, default = 10u64)]
|
||||
time_range: RefCell<u64>,
|
||||
|
||||
#[property(get, set, nick = "Flip X", blurb = "Flip the x axis", default = true)]
|
||||
flip_x: RefCell<bool>,
|
||||
#[property(get, set, nick = "Flip Y", blurb = "Flip the y axis", default = true)]
|
||||
flip_y: RefCell<bool>,
|
||||
#[property(get, set, nick = "Vertical", blurb = "Exchange the x and y axes", default = false)]
|
||||
vertical: RefCell<bool>,
|
||||
|
||||
history: RefCell<VecDeque<(std::time::Instant, f64)>>,
|
||||
extra_point: RefCell<Option<(std::time::Instant, f64)>>,
|
||||
last_updated_at: RefCell<std::time::Instant>,
|
||||
}
|
||||
|
||||
impl Default for GraphPriv {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
value: RefCell::new(0.0),
|
||||
thickness: RefCell::new(1.0),
|
||||
line_style: RefCell::new("miter".to_string()),
|
||||
min: RefCell::new(0.0),
|
||||
max: RefCell::new(100.0),
|
||||
dynamic: RefCell::new(true),
|
||||
time_range: RefCell::new(10),
|
||||
flip_x: RefCell::new(true),
|
||||
flip_y: RefCell::new(true),
|
||||
vertical: RefCell::new(false),
|
||||
history: RefCell::new(VecDeque::new()),
|
||||
extra_point: RefCell::new(None),
|
||||
last_updated_at: RefCell::new(std::time::Instant::now()),
|
||||
cached_path: RefCell::new(None),
|
||||
path_anchor_time: Cell::new(Instant::now()),
|
||||
cached_path_size: Cell::new((0.0, 0.0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GraphPriv {
|
||||
// Updates the history, removing points ouside the range
|
||||
fn update_history(&self, v: (std::time::Instant, f64)) {
|
||||
let mut history = self.history.borrow_mut();
|
||||
let mut last_value = self.extra_point.borrow_mut();
|
||||
let mut last_updated_at = self.last_updated_at.borrow_mut();
|
||||
*last_updated_at = std::time::Instant::now();
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for Graph {
|
||||
const NAME: &'static str = "EwwiiGraph";
|
||||
type Type = super::Graph;
|
||||
type ParentType = gtk4::Widget;
|
||||
}
|
||||
|
||||
while let Some(entry) = history.front() {
|
||||
if last_updated_at.duration_since(entry.0).as_millis() as u64
|
||||
> *self.time_range.borrow()
|
||||
{
|
||||
*last_value = history.pop_front();
|
||||
impl ObjectImpl for Graph {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
let obj = self.obj();
|
||||
obj.add_css_class("graph");
|
||||
|
||||
if DEFAULT_ANIMATE {
|
||||
self.set_animate(&obj, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
use once_cell::sync::Lazy;
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecDouble::builder("value")
|
||||
.minimum(0.0)
|
||||
.maximum(f64::MAX)
|
||||
.default_value(DEFAULT_VALUE)
|
||||
.build(),
|
||||
glib::ParamSpecDouble::builder("thickness")
|
||||
.minimum(0.0)
|
||||
.maximum(f64::MAX)
|
||||
.default_value(DEFAULT_THICKNESS)
|
||||
.build(),
|
||||
glib::ParamSpecEnum::builder::<LineStyle>("line-style")
|
||||
.default_value(LineStyle::default())
|
||||
.build(),
|
||||
glib::ParamSpecDouble::builder("min")
|
||||
.minimum(f64::MIN)
|
||||
.maximum(f64::MAX)
|
||||
.default_value(DEFAULT_MIN)
|
||||
.build(),
|
||||
glib::ParamSpecDouble::builder("max")
|
||||
.minimum(f64::MIN)
|
||||
.maximum(f64::MAX)
|
||||
.default_value(DEFAULT_MAX)
|
||||
.build(),
|
||||
glib::ParamSpecBoolean::builder("dynamic")
|
||||
.default_value(DEFAULT_DYNAMIC)
|
||||
.build(),
|
||||
glib::ParamSpecUInt::builder("time-range")
|
||||
.minimum(0)
|
||||
.maximum(u32::MAX)
|
||||
.default_value(DEFAULT_TIME_RANGE)
|
||||
.build(),
|
||||
glib::ParamSpecBoolean::builder("flip-x").default_value(DEFAULT_FLIP_X).build(),
|
||||
glib::ParamSpecBoolean::builder("flip-y").default_value(DEFAULT_FLIP_Y).build(),
|
||||
glib::ParamSpecBoolean::builder("vertical")
|
||||
.default_value(DEFAULT_VERTICAL)
|
||||
.build(),
|
||||
glib::ParamSpecEnum::builder::<RenderType>("type")
|
||||
.default_value(RenderType::default())
|
||||
.build(),
|
||||
glib::ParamSpecBoolean::builder("animate")
|
||||
.default_value(DEFAULT_ANIMATE)
|
||||
.build(),
|
||||
]
|
||||
});
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
let needs_path_rebuild = match pspec.name() {
|
||||
"value" => {
|
||||
let v: f64 = value.get().unwrap();
|
||||
self.value.set(v);
|
||||
|
||||
// Don't record first value (default value)
|
||||
if self.has_received_value.get() {
|
||||
self.update_history((Instant::now(), v));
|
||||
} else {
|
||||
self.has_received_value.replace(true);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
"thickness" => {
|
||||
self.thickness.set(value.get().unwrap());
|
||||
false // Thickness doesn't affect path geometry
|
||||
}
|
||||
"line-style" => {
|
||||
self.line_style.set(value.get::<LineStyle>().unwrap());
|
||||
false // Line style doesn't affect path geometry
|
||||
}
|
||||
"min" => {
|
||||
self.min.set(value.get().unwrap());
|
||||
true
|
||||
}
|
||||
"max" => {
|
||||
self.max.set(value.get().unwrap());
|
||||
true
|
||||
}
|
||||
"dynamic" => {
|
||||
self.dynamic.set(value.get().unwrap());
|
||||
true
|
||||
}
|
||||
"time-range" => {
|
||||
self.time_range.set(value.get().unwrap());
|
||||
true
|
||||
}
|
||||
"flip-x" => {
|
||||
self.flip_x.set(value.get().unwrap());
|
||||
true
|
||||
}
|
||||
"flip-y" => {
|
||||
self.flip_y.set(value.get().unwrap());
|
||||
true
|
||||
}
|
||||
"vertical" => {
|
||||
self.vertical.set(value.get().unwrap());
|
||||
true
|
||||
}
|
||||
"type" => {
|
||||
self.render_type.set(value.get::<RenderType>().unwrap());
|
||||
true
|
||||
}
|
||||
"animate" => {
|
||||
let animate = value.get().unwrap();
|
||||
self.set_animate(&self.obj(), animate);
|
||||
false
|
||||
}
|
||||
x => panic!("Tried to set inexistent property of Graph: {}", x),
|
||||
};
|
||||
|
||||
if needs_path_rebuild {
|
||||
self.cached_path.replace(None);
|
||||
}
|
||||
|
||||
// Queue redraw for any property change
|
||||
self.obj().queue_draw();
|
||||
}
|
||||
|
||||
fn property(&self, _: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"value" => self.value.get().to_value(),
|
||||
"thickness" => self.thickness.get().to_value(),
|
||||
"line-style" => self.line_style.get().to_value(),
|
||||
"min" => self.min.get().to_value(),
|
||||
"max" => self.max.get().to_value(),
|
||||
"dynamic" => self.dynamic.get().to_value(),
|
||||
"time-range" => (self.time_range.get()).to_value(),
|
||||
"flip-x" => self.flip_x.get().to_value(),
|
||||
"flip-y" => self.flip_y.get().to_value(),
|
||||
"vertical" => self.vertical.get().to_value(),
|
||||
"type" => self.render_type.get().to_value(),
|
||||
"animate" => self.is_animate().to_value(),
|
||||
x => panic!("Tried to get inexistent property of Graph: {}", x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GraphGeometry {
|
||||
width: f32,
|
||||
height: f32,
|
||||
flip_x: bool,
|
||||
flip_y: bool,
|
||||
vertical: bool,
|
||||
}
|
||||
|
||||
struct GraphRange {
|
||||
min: f64,
|
||||
max: f64,
|
||||
time_range: f32,
|
||||
anchor_time: Instant,
|
||||
}
|
||||
|
||||
struct GraphStyle {
|
||||
render_type: RenderType,
|
||||
thickness: f32,
|
||||
line_style: LineStyle,
|
||||
color: gdk::RGBA,
|
||||
animate: bool,
|
||||
}
|
||||
|
||||
impl WidgetImpl for Graph {
|
||||
fn measure(&self, _orientation: gtk4::Orientation, _for_size: i32) -> (i32, i32, i32, i32) {
|
||||
let t = self.thickness.get().max(1.0) as i32;
|
||||
// min, natural, -, -
|
||||
(t, t * 4, -1, -1)
|
||||
}
|
||||
|
||||
/// Snapshot render nodes.
|
||||
///
|
||||
/// Since this is potentially a hot code path, we want to keep it O(N) for maximum performance.
|
||||
/// - min/max value calculation cached
|
||||
/// - graph curve path cached
|
||||
fn snapshot(&self, snapshot: >k4::Snapshot) {
|
||||
let obj = self.obj();
|
||||
let history = self.history.borrow();
|
||||
|
||||
if history.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Margins
|
||||
let margin_start = obj.margin_start() as f32;
|
||||
let margin_end = obj.margin_end() as f32;
|
||||
let margin_top = obj.margin_top() as f32;
|
||||
let margin_bottom = obj.margin_bottom() as f32;
|
||||
|
||||
// Allocated size
|
||||
let total_width = obj.width() as f32;
|
||||
let total_height = obj.height() as f32;
|
||||
let width = (total_width - margin_start - margin_end).max(0.0);
|
||||
let height = (total_height - margin_top - margin_bottom).max(0.0);
|
||||
|
||||
let geom = GraphGeometry {
|
||||
width,
|
||||
height,
|
||||
flip_x: self.flip_x.get(),
|
||||
flip_y: self.flip_y.get(),
|
||||
vertical: self.vertical.get(),
|
||||
};
|
||||
|
||||
let style = GraphStyle {
|
||||
render_type: self.render_type.get(),
|
||||
thickness: self.thickness.get() as f32,
|
||||
line_style: self.line_style.get(),
|
||||
color: obj.color(),
|
||||
animate: self.is_animate(),
|
||||
};
|
||||
|
||||
let time_range_millis = self.time_range.get();
|
||||
if time_range_millis == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let render_time = Instant::now();
|
||||
let time_range = time_range_millis as f32;
|
||||
|
||||
// Rebuild path?
|
||||
let current_size = (width, height);
|
||||
let mut cached_path = self.cached_path.borrow_mut();
|
||||
if cached_path.is_none() || self.cached_path_size.get() != current_size {
|
||||
self.cached_path_size.set(current_size);
|
||||
self.path_anchor_time.set(render_time);
|
||||
|
||||
let range = GraphRange {
|
||||
min: self.min_value(),
|
||||
max: self.max_value(),
|
||||
time_range,
|
||||
anchor_time: render_time,
|
||||
};
|
||||
|
||||
let new_path = build_path(&history, &geom, &range, &style);
|
||||
*cached_path = Some(new_path);
|
||||
}
|
||||
|
||||
// Draw the cached path with translation
|
||||
snapshot.save();
|
||||
snapshot.translate(&graphene::Point::new(margin_start, margin_top));
|
||||
snapshot.push_clip(&graphene::Rect::new(0.0, 0.0, width, height));
|
||||
|
||||
// Scroll animation
|
||||
if style.animate && history.len() >= 2 {
|
||||
// Shift by point interval to hide the gap between latest point and right edge
|
||||
let interval = {
|
||||
let t_old = history[history.len() - 2].0;
|
||||
let t_new = history[history.len() - 1].0;
|
||||
t_new.checked_duration_since(t_old).unwrap_or_default()
|
||||
}
|
||||
.as_millis() as f32;
|
||||
|
||||
// Calculate pixel shift based on time
|
||||
let anchor_time = self.path_anchor_time.get();
|
||||
let time_shift = render_time.duration_since(anchor_time).as_millis() as f32;
|
||||
let pixel_shift = (time_shift - interval) / time_range
|
||||
* if geom.vertical { geom.height } else { geom.width }
|
||||
* if geom.flip_x { 1.0 } else { -1.0 };
|
||||
|
||||
// Apply the camera translation
|
||||
if geom.vertical {
|
||||
snapshot.translate(&graphene::Point::new(0.0, pixel_shift));
|
||||
} else {
|
||||
snapshot.translate(&graphene::Point::new(pixel_shift, 0.0));
|
||||
}
|
||||
};
|
||||
|
||||
// Draw path
|
||||
if let Some(path) = cached_path.as_ref() {
|
||||
if matches!(style.render_type, RenderType::Line | RenderType::StepLine) {
|
||||
// Render as stroked path
|
||||
let stroke = gsk::Stroke::new(style.thickness);
|
||||
|
||||
// Configure line style
|
||||
match style.line_style {
|
||||
LineStyle::Miter => {
|
||||
stroke.set_line_cap(gsk::LineCap::Butt);
|
||||
stroke.set_line_join(gsk::LineJoin::Miter);
|
||||
}
|
||||
LineStyle::Bevel => {
|
||||
stroke.set_line_cap(gsk::LineCap::Square);
|
||||
stroke.set_line_join(gsk::LineJoin::Bevel);
|
||||
}
|
||||
LineStyle::Round => {
|
||||
stroke.set_line_cap(gsk::LineCap::Round);
|
||||
stroke.set_line_join(gsk::LineJoin::Round);
|
||||
}
|
||||
}
|
||||
|
||||
snapshot.append_stroke(path, &stroke, &style.color);
|
||||
} else {
|
||||
// Render as filled path
|
||||
snapshot.append_fill(path, gsk::FillRule::Winding, &style.color);
|
||||
}
|
||||
}
|
||||
|
||||
snapshot.pop(); // pop clip
|
||||
snapshot.restore(); // restore translate
|
||||
}
|
||||
}
|
||||
|
||||
impl Graph {
|
||||
// Updates the history, removing points outside the range
|
||||
fn update_history(&self, v: (Instant, f64)) {
|
||||
let mut history = self.history.borrow_mut();
|
||||
let now = Instant::now();
|
||||
self.last_updated_at.set(now);
|
||||
|
||||
let time_range_dur = Duration::from_millis(self.time_range.get().into());
|
||||
let visible_start = now
|
||||
.checked_sub(time_range_dur)
|
||||
.unwrap_or_else(|| history.front().map(|(t, _)| *t).unwrap_or(now));
|
||||
|
||||
// Animate: Need one extra point more left of the visible area to avoid showing a gap
|
||||
let points_to_keep_off_canvas = if self.is_animate() { 2 } else { 1 };
|
||||
|
||||
// Prune history from the front (oldest).
|
||||
while history.len() > points_to_keep_off_canvas {
|
||||
// We check the timestamp of the point at index `points_to_keep_off_canvas`.
|
||||
// If that point is outside the canvas, we can safely remove the oldest point (index 0).
|
||||
if history[points_to_keep_off_canvas].0 < visible_start {
|
||||
if let Some(val) = history.pop_front() {
|
||||
// Value dropped: min/max values need recalc?
|
||||
if self.min_value_cached.get().map_or(false, |min| min == val.1) {
|
||||
self.min_value_cached.set(None);
|
||||
}
|
||||
if self.max_value_cached.get().map_or(false, |max| max == val.1) {
|
||||
self.max_value_cached.set(None);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
history.push_back(v);
|
||||
|
||||
// New value: Update cached min/max value?
|
||||
if let Some(min) = self.min_value_cached.get() {
|
||||
self.min_value_cached.set(Some(min.min(v.1)));
|
||||
}
|
||||
if let Some(max) = self.max_value_cached.get() {
|
||||
self.max_value_cached.set(Some(max.max(v.1)));
|
||||
}
|
||||
}
|
||||
|
||||
fn set_animate(&self, obj: &super::Graph, animate: bool) {
|
||||
let mut tick_id_storage = self.tick_id.borrow_mut();
|
||||
|
||||
if animate {
|
||||
// If we want it ON, and it's currently None (OFF)
|
||||
if tick_id_storage.is_none() {
|
||||
let id = obj.add_tick_callback(|obj, _clock| {
|
||||
obj.queue_draw();
|
||||
glib::ControlFlow::Continue
|
||||
});
|
||||
*tick_id_storage = Some(id);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
// We want it OFF
|
||||
if let Some(id) = tick_id_storage.take() {
|
||||
id.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
history.push_back(v);
|
||||
}
|
||||
/**
|
||||
* Receives normalized (0-1) coordinates `x` and `y` and convert them to the
|
||||
* point on the widget.
|
||||
*/
|
||||
fn value_to_point(&self, width: f64, height: f64, x: f64, y: f64) -> (f64, f64) {
|
||||
let x = if *self.flip_x.borrow() { 1.0 - x } else { x };
|
||||
let y = if *self.flip_y.borrow() { 1.0 - y } else { y };
|
||||
let (x, y) = if *self.vertical.borrow() { (y, x) } else { (x, y) };
|
||||
(width * x, height * y)
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for GraphPriv {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
Self::derived_properties()
|
||||
}
|
||||
fn is_animate(&self) -> bool {
|
||||
self.tick_id.borrow().is_some()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"value" => {
|
||||
let value = value.get().unwrap();
|
||||
self.value.replace(value);
|
||||
self.update_history((std::time::Instant::now(), value));
|
||||
self.obj().queue_draw();
|
||||
fn min_value(&self) -> f64 {
|
||||
if self.dynamic.get() {
|
||||
self.min_value_cached.get().unwrap_or_else(|| {
|
||||
// Calculate from history values
|
||||
let history = self.history.borrow();
|
||||
let val = history.iter().fold(f64::INFINITY, |acc, &(_, v)| acc.min(v));
|
||||
self.min_value_cached.set(Some(val));
|
||||
val
|
||||
})
|
||||
} else {
|
||||
self.min.get()
|
||||
}
|
||||
"thickness" => {
|
||||
self.thickness.replace(value.get().unwrap());
|
||||
}
|
||||
|
||||
fn max_value(&self) -> f64 {
|
||||
if self.dynamic.get() {
|
||||
self.max_value_cached.get().unwrap_or_else(|| {
|
||||
// Calculate from history values
|
||||
let history = self.history.borrow();
|
||||
let val = history.iter().fold(f64::NEG_INFINITY, |acc, &(_, v)| acc.max(v));
|
||||
self.max_value_cached.set(Some(val));
|
||||
val
|
||||
})
|
||||
} else {
|
||||
self.max.get()
|
||||
}
|
||||
"max" => {
|
||||
self.max.replace(value.get().unwrap());
|
||||
}
|
||||
"min" => {
|
||||
self.min.replace(value.get().unwrap());
|
||||
}
|
||||
"dynamic" => {
|
||||
self.dynamic.replace(value.get().unwrap());
|
||||
}
|
||||
"time-range" => {
|
||||
self.time_range.replace(value.get().unwrap());
|
||||
}
|
||||
"line-style" => {
|
||||
self.line_style.replace(value.get().unwrap());
|
||||
}
|
||||
"flip-x" => {
|
||||
self.flip_x.replace(value.get().unwrap());
|
||||
}
|
||||
"flip-y" => {
|
||||
self.flip_y.replace(value.get().unwrap());
|
||||
}
|
||||
"vertical" => {
|
||||
self.vertical.replace(value.get().unwrap());
|
||||
}
|
||||
x => panic!("Tried to set inexistant property of Graph: {}", x,),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
self.derived_property(id, pspec)
|
||||
/// Builds a path relative to a fixed anchor time
|
||||
fn build_path(
|
||||
points: &VecDeque<(Instant, f64)>,
|
||||
geom: &GraphGeometry,
|
||||
range: &GraphRange,
|
||||
style: &GraphStyle,
|
||||
) -> gsk::Path {
|
||||
if points.is_empty() {
|
||||
return gsk::PathBuilder::new().to_path();
|
||||
}
|
||||
|
||||
if range.time_range <= 0.0 {
|
||||
return gsk::PathBuilder::new().to_path();
|
||||
}
|
||||
|
||||
let builder = gsk::PathBuilder::new();
|
||||
let is_line = matches!(style.render_type, RenderType::Line | RenderType::StepLine);
|
||||
let is_step = matches!(style.render_type, RenderType::StepLine | RenderType::StepFill);
|
||||
|
||||
// Transform point relative to fixed anchor
|
||||
let transform_point = |t_point: Instant, value: f64| -> (f32, f32) {
|
||||
let t = range.anchor_time.duration_since(t_point).as_millis() as f32;
|
||||
|
||||
let nx = t / range.time_range;
|
||||
let ny = ((value - range.min) / (range.max - range.min).max(f64::EPSILON)) as f32;
|
||||
|
||||
let x = if geom.flip_x { nx } else { 1.0 - nx };
|
||||
let y = if geom.flip_y { ny } else { 1.0 - ny };
|
||||
|
||||
if geom.vertical {
|
||||
(y * geom.width, x * geom.height)
|
||||
} else {
|
||||
(x * geom.width, y * geom.height)
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate first point coordinates
|
||||
let &(t0, v0) = &points[0];
|
||||
let (mut last_x, mut last_y) = transform_point(t0, v0);
|
||||
|
||||
let (base_x, base_y) = transform_point(range.anchor_time, range.min);
|
||||
|
||||
if is_line {
|
||||
// For lines: start at first point
|
||||
builder.move_to(last_x, last_y);
|
||||
} else {
|
||||
// For fills: start at baseline then go to first point
|
||||
if geom.vertical {
|
||||
builder.move_to(base_x, last_y);
|
||||
builder.line_to(last_x, last_y);
|
||||
} else {
|
||||
builder.move_to(last_x, base_y);
|
||||
builder.line_to(last_x, last_y);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the main graph line
|
||||
for i in 1..points.len() {
|
||||
let &(_, v_prev) = &points[i - 1];
|
||||
let &(t_curr, v_curr) = &points[i];
|
||||
|
||||
let (x_curr, y_curr) = transform_point(t_curr, v_curr);
|
||||
|
||||
if is_step {
|
||||
// Create horizontal step segment
|
||||
let (x_step, y_step) = transform_point(t_curr, v_prev);
|
||||
builder.line_to(x_step, y_step);
|
||||
}
|
||||
|
||||
builder.line_to(x_curr, y_curr);
|
||||
last_x = x_curr;
|
||||
last_y = y_curr;
|
||||
}
|
||||
|
||||
if is_line {
|
||||
// For lines, we're done
|
||||
builder.to_path()
|
||||
} else {
|
||||
// For fills, close the path to baseline
|
||||
if geom.vertical {
|
||||
builder.line_to(base_x, last_y);
|
||||
} else {
|
||||
builder.line_to(last_x, base_y);
|
||||
}
|
||||
builder.close();
|
||||
builder.to_path()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[object_subclass]
|
||||
impl ObjectSubclass for GraphPriv {
|
||||
type ParentType = gtk::Bin;
|
||||
type Type = Graph;
|
||||
|
||||
const NAME: &'static str = "Graph";
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.set_css_name("graph");
|
||||
}
|
||||
#[derive(glib::Enum, Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[enum_type(name = "EwwiiGraphLineStyle")]
|
||||
pub enum LineStyle {
|
||||
#[default]
|
||||
Miter,
|
||||
Bevel,
|
||||
Round,
|
||||
}
|
||||
|
||||
impl Default for Graph {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
#[derive(glib::Enum, Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[enum_type(name = "EwwiiGraphRenderType")]
|
||||
pub enum RenderType {
|
||||
#[default]
|
||||
Line,
|
||||
StepLine,
|
||||
Fill,
|
||||
StepFill,
|
||||
}
|
||||
|
||||
// public wrapper
|
||||
glib::wrapper! {
|
||||
pub struct Graph(ObjectSubclass<imp::Graph>)
|
||||
@extends gtk4::Widget,
|
||||
@implements gtk4::Accessible, gtk4::Actionable, gtk4::Buildable, gtk4::ConstraintTarget;
|
||||
}
|
||||
|
||||
impl Graph {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new::<Self>()
|
||||
glib::Object::builder().build()
|
||||
}
|
||||
}
|
||||
|
||||
impl ContainerImpl for GraphPriv {
|
||||
fn add(&self, _widget: >k::Widget) {
|
||||
error_handling_ctx::print_error(anyhow!("Error, Graph widget shoudln't have any children"));
|
||||
}
|
||||
}
|
||||
|
||||
impl BinImpl for GraphPriv {}
|
||||
impl WidgetImpl for GraphPriv {
|
||||
fn preferred_width(&self) -> (i32, i32) {
|
||||
let thickness = *self.thickness.borrow() as i32;
|
||||
(thickness, thickness)
|
||||
}
|
||||
|
||||
fn preferred_width_for_height(&self, height: i32) -> (i32, i32) {
|
||||
(height, height)
|
||||
}
|
||||
|
||||
fn preferred_height(&self) -> (i32, i32) {
|
||||
let thickness = *self.thickness.borrow() as i32;
|
||||
(thickness, thickness)
|
||||
}
|
||||
|
||||
fn preferred_height_for_width(&self, width: i32) -> (i32, i32) {
|
||||
(width, width)
|
||||
}
|
||||
|
||||
fn draw(&self, cr: &cairo::Context) -> glib::Propagation {
|
||||
let res: Result<()> = (|| {
|
||||
let history = &*self.history.borrow();
|
||||
let extra_point = *self.extra_point.borrow();
|
||||
|
||||
// Calculate the max value
|
||||
let (min, max) = {
|
||||
let mut max = *self.max.borrow();
|
||||
let min = *self.min.borrow();
|
||||
let dynamic = *self.dynamic.borrow();
|
||||
if dynamic {
|
||||
// Check for points higher than max
|
||||
for (_, value) in history {
|
||||
if *value > max {
|
||||
max = *value;
|
||||
}
|
||||
}
|
||||
if let Some((_, value)) = extra_point {
|
||||
if value > max {
|
||||
max = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
(min, max)
|
||||
};
|
||||
|
||||
let styles = self.obj().style_context();
|
||||
let (margin_top, margin_right, margin_bottom, margin_left) = {
|
||||
let margin = styles.margin(gtk::StateFlags::NORMAL);
|
||||
(margin.top as f64, margin.right as f64, margin.bottom as f64, margin.left as f64)
|
||||
};
|
||||
let width = self.obj().allocated_width() as f64 - margin_left - margin_right;
|
||||
let height = self.obj().allocated_height() as f64 - margin_top - margin_bottom;
|
||||
|
||||
// Calculate graph points once
|
||||
// Separating this into another function would require pasing a
|
||||
// GraphPriv that would hide interior mutability
|
||||
let points = {
|
||||
let value_range = max - min;
|
||||
let time_range = *self.time_range.borrow() as f64;
|
||||
let last_updated_at = self.last_updated_at.borrow();
|
||||
let mut points = history
|
||||
.iter()
|
||||
.map(|(instant, value)| {
|
||||
let t = last_updated_at.duration_since(*instant).as_millis() as f64;
|
||||
self.value_to_point(
|
||||
width,
|
||||
height,
|
||||
t / time_range,
|
||||
(value - min) / value_range,
|
||||
)
|
||||
})
|
||||
.collect::<VecDeque<(f64, f64)>>();
|
||||
|
||||
// Aad an extra point outside of the graph to extend the line to the left
|
||||
if let Some((instant, value)) = extra_point {
|
||||
let t = last_updated_at.duration_since(instant).as_millis() as f64;
|
||||
let (x, y) = self.value_to_point(
|
||||
width,
|
||||
height,
|
||||
(t - time_range) / time_range,
|
||||
(value - min) / value_range,
|
||||
);
|
||||
points.push_front(if *self.vertical.borrow() { (x, -y) } else { (-x, y) });
|
||||
}
|
||||
points
|
||||
};
|
||||
|
||||
// Actually draw the graph
|
||||
cr.save()?;
|
||||
cr.translate(margin_left, margin_top);
|
||||
cr.rectangle(0.0, 0.0, width, height);
|
||||
cr.clip();
|
||||
|
||||
// Draw Background
|
||||
let bg_color: gdk::RGBA = styles
|
||||
.style_property_for_state("background-color", gtk::StateFlags::NORMAL)
|
||||
.get()?;
|
||||
if bg_color.alpha() > 0.0 {
|
||||
if let Some(first_point) = points.front() {
|
||||
cr.line_to(first_point.0, height + margin_bottom);
|
||||
}
|
||||
for (x, y) in points.iter() {
|
||||
cr.line_to(*x, *y);
|
||||
}
|
||||
cr.line_to(width, height);
|
||||
|
||||
cr.set_source_rgba(
|
||||
bg_color.red(),
|
||||
bg_color.green(),
|
||||
bg_color.blue(),
|
||||
bg_color.alpha(),
|
||||
);
|
||||
cr.fill()?;
|
||||
}
|
||||
|
||||
// Draw Line
|
||||
let line_color: gdk::RGBA = styles.color(gtk::StateFlags::NORMAL);
|
||||
let thickness = *self.thickness.borrow();
|
||||
if line_color.alpha() > 0.0 && thickness > 0.0 {
|
||||
for (x, y) in points.iter() {
|
||||
cr.line_to(*x, *y);
|
||||
}
|
||||
|
||||
let line_style = &*self.line_style.borrow();
|
||||
apply_line_style(line_style.as_str(), cr)?;
|
||||
cr.set_line_width(thickness);
|
||||
cr.set_source_rgba(
|
||||
line_color.red(),
|
||||
line_color.green(),
|
||||
line_color.blue(),
|
||||
line_color.alpha(),
|
||||
);
|
||||
cr.stroke()?;
|
||||
}
|
||||
|
||||
cr.reset_clip();
|
||||
cr.restore()?;
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
if let Err(error) = res {
|
||||
error_handling_ctx::print_error(error)
|
||||
};
|
||||
|
||||
glib::Propagation::Proceed
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_line_style(style: &str, cr: &cairo::Context) -> Result<()> {
|
||||
match style {
|
||||
"miter" => {
|
||||
cr.set_line_cap(cairo::LineCap::Butt);
|
||||
cr.set_line_join(cairo::LineJoin::Miter);
|
||||
}
|
||||
"bevel" => {
|
||||
cr.set_line_cap(cairo::LineCap::Square);
|
||||
cr.set_line_join(cairo::LineJoin::Bevel);
|
||||
}
|
||||
"round" => {
|
||||
cr.set_line_cap(cairo::LineCap::Round);
|
||||
cr.set_line_join(cairo::LineJoin::Round);
|
||||
}
|
||||
_ => Err(anyhow!("Error, the value: {} for atribute join is not valid", style))?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,4 +4,3 @@ pub mod graph;
|
||||
pub mod transform;
|
||||
pub mod widget_definitions;
|
||||
pub mod widget_definitions_helper;
|
||||
pub mod window;
|
||||
|
||||
@@ -1,214 +1,214 @@
|
||||
use crate::window::coords::NumWithUnit;
|
||||
use anyhow::{anyhow, Result};
|
||||
use gtk::glib::{self, object_subclass, wrapper, Properties};
|
||||
use gtk::{prelude::*, subclass::prelude::*};
|
||||
use std::{cell::RefCell, str::FromStr};
|
||||
// use crate::window::coords::NumWithUnit;
|
||||
// use anyhow::{anyhow, Result};
|
||||
// use gtk4::glib::{self, object_subclass, wrapper, Properties};
|
||||
// use gtk4::{prelude::*, subclass::prelude::*};
|
||||
// use std::{cell::RefCell, str::FromStr};
|
||||
|
||||
use crate::error_handling_ctx;
|
||||
// use crate::error_handling_ctx;
|
||||
|
||||
wrapper! {
|
||||
pub struct Transform(ObjectSubclass<TransformPriv>)
|
||||
@extends gtk::Bin, gtk::Container, gtk::Widget;
|
||||
}
|
||||
// wrapper! {
|
||||
// pub struct Transform(ObjectSubclass<TransformPriv>)
|
||||
// @extends gtk4::Bin, gtk4::Container, gtk4::Widget;
|
||||
// }
|
||||
|
||||
#[derive(Properties)]
|
||||
#[properties(wrapper_type = Transform)]
|
||||
pub struct TransformPriv {
|
||||
#[property(get, set, nick = "Rotate", blurb = "The Rotation", minimum = f64::MIN, maximum = f64::MAX, default = 0f64)]
|
||||
rotate: RefCell<f64>,
|
||||
// #[derive(Properties)]
|
||||
// #[properties(wrapper_type = Transform)]
|
||||
// pub struct TransformPriv {
|
||||
// #[property(get, set, nick = "Rotate", blurb = "The Rotation", minimum = f64::MIN, maximum = f64::MAX, default = 0f64)]
|
||||
// rotate: RefCell<f64>,
|
||||
|
||||
#[property(get, set, nick = "Transform-Origin X", blurb = "X coordinate (%/px) for the Transform-Origin", default = None)]
|
||||
transform_origin_x: RefCell<Option<String>>,
|
||||
// #[property(get, set, nick = "Transform-Origin X", blurb = "X coordinate (%/px) for the Transform-Origin", default = None)]
|
||||
// transform_origin_x: RefCell<Option<String>>,
|
||||
|
||||
#[property(get, set, nick = "Transform-Origin Y", blurb = "Y coordinate (%/px) for the Transform-Origin", default = None)]
|
||||
transform_origin_y: RefCell<Option<String>>,
|
||||
// #[property(get, set, nick = "Transform-Origin Y", blurb = "Y coordinate (%/px) for the Transform-Origin", default = None)]
|
||||
// transform_origin_y: RefCell<Option<String>>,
|
||||
|
||||
#[property(get, set, nick = "Translate x", blurb = "The X Translation", default = None)]
|
||||
translate_x: RefCell<Option<String>>,
|
||||
// #[property(get, set, nick = "Translate x", blurb = "The X Translation", default = None)]
|
||||
// translate_x: RefCell<Option<String>>,
|
||||
|
||||
#[property(get, set, nick = "Translate y", blurb = "The Y Translation", default = None)]
|
||||
translate_y: RefCell<Option<String>>,
|
||||
// #[property(get, set, nick = "Translate y", blurb = "The Y Translation", default = None)]
|
||||
// translate_y: RefCell<Option<String>>,
|
||||
|
||||
#[property(get, set, nick = "Scale x", blurb = "The amount to scale in x", default = None)]
|
||||
scale_x: RefCell<Option<String>>,
|
||||
// #[property(get, set, nick = "Scale x", blurb = "The amount to scale in x", default = None)]
|
||||
// scale_x: RefCell<Option<String>>,
|
||||
|
||||
#[property(get, set, nick = "Scale y", blurb = "The amount to scale in y", default = None)]
|
||||
scale_y: RefCell<Option<String>>,
|
||||
// #[property(get, set, nick = "Scale y", blurb = "The amount to scale in y", default = None)]
|
||||
// scale_y: RefCell<Option<String>>,
|
||||
|
||||
content: RefCell<Option<gtk::Widget>>,
|
||||
}
|
||||
// content: RefCell<Option<gtk4::Widget>>,
|
||||
// }
|
||||
|
||||
// This should match the default values from the ParamSpecs
|
||||
impl Default for TransformPriv {
|
||||
fn default() -> Self {
|
||||
TransformPriv {
|
||||
rotate: RefCell::new(0.0),
|
||||
transform_origin_x: RefCell::new(None),
|
||||
transform_origin_y: RefCell::new(None),
|
||||
translate_x: RefCell::new(None),
|
||||
translate_y: RefCell::new(None),
|
||||
scale_x: RefCell::new(None),
|
||||
scale_y: RefCell::new(None),
|
||||
content: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
// // This should match the default values from the ParamSpecs
|
||||
// impl Default for TransformPriv {
|
||||
// fn default() -> Self {
|
||||
// TransformPriv {
|
||||
// rotate: RefCell::new(0.0),
|
||||
// transform_origin_x: RefCell::new(None),
|
||||
// transform_origin_y: RefCell::new(None),
|
||||
// translate_x: RefCell::new(None),
|
||||
// translate_y: RefCell::new(None),
|
||||
// scale_x: RefCell::new(None),
|
||||
// scale_y: RefCell::new(None),
|
||||
// content: RefCell::new(None),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
impl ObjectImpl for TransformPriv {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
Self::derived_properties()
|
||||
}
|
||||
// impl ObjectImpl for TransformPriv {
|
||||
// fn properties() -> &'static [glib::ParamSpec] {
|
||||
// Self::derived_properties()
|
||||
// }
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"rotate" => {
|
||||
self.rotate.replace(value.get().unwrap());
|
||||
self.obj().queue_draw(); // Queue a draw call with the updated value
|
||||
}
|
||||
"transform-origin-x" => {
|
||||
self.transform_origin_x.replace(value.get().unwrap());
|
||||
self.obj().queue_draw(); // Queue a draw call with the updated value
|
||||
}
|
||||
"transform-origin-y" => {
|
||||
self.transform_origin_y.replace(value.get().unwrap());
|
||||
self.obj().queue_draw(); // Queue a draw call with the updated value
|
||||
}
|
||||
"translate-x" => {
|
||||
self.translate_x.replace(value.get().unwrap());
|
||||
self.obj().queue_draw(); // Queue a draw call with the updated value
|
||||
}
|
||||
"translate-y" => {
|
||||
self.translate_y.replace(value.get().unwrap());
|
||||
self.obj().queue_draw(); // Queue a draw call with the updated value
|
||||
}
|
||||
"scale-x" => {
|
||||
self.scale_x.replace(value.get().unwrap());
|
||||
self.obj().queue_draw(); // Queue a draw call with the updated value
|
||||
}
|
||||
"scale-y" => {
|
||||
self.scale_y.replace(value.get().unwrap());
|
||||
self.obj().queue_draw(); // Queue a draw call with the updated value
|
||||
}
|
||||
x => panic!("Tried to set inexistant property of Transform: {}", x,),
|
||||
}
|
||||
}
|
||||
// fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
// match pspec.name() {
|
||||
// "rotate" => {
|
||||
// self.rotate.replace(value.get().unwrap());
|
||||
// self.obj().queue_draw(); // Queue a draw call with the updated value
|
||||
// }
|
||||
// "transform-origin-x" => {
|
||||
// self.transform_origin_x.replace(value.get().unwrap());
|
||||
// self.obj().queue_draw(); // Queue a draw call with the updated value
|
||||
// }
|
||||
// "transform-origin-y" => {
|
||||
// self.transform_origin_y.replace(value.get().unwrap());
|
||||
// self.obj().queue_draw(); // Queue a draw call with the updated value
|
||||
// }
|
||||
// "translate-x" => {
|
||||
// self.translate_x.replace(value.get().unwrap());
|
||||
// self.obj().queue_draw(); // Queue a draw call with the updated value
|
||||
// }
|
||||
// "translate-y" => {
|
||||
// self.translate_y.replace(value.get().unwrap());
|
||||
// self.obj().queue_draw(); // Queue a draw call with the updated value
|
||||
// }
|
||||
// "scale-x" => {
|
||||
// self.scale_x.replace(value.get().unwrap());
|
||||
// self.obj().queue_draw(); // Queue a draw call with the updated value
|
||||
// }
|
||||
// "scale-y" => {
|
||||
// self.scale_y.replace(value.get().unwrap());
|
||||
// self.obj().queue_draw(); // Queue a draw call with the updated value
|
||||
// }
|
||||
// x => panic!("Tried to set inexistant property of Transform: {}", x,),
|
||||
// }
|
||||
// }
|
||||
|
||||
fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
self.derived_property(id, pspec)
|
||||
}
|
||||
}
|
||||
// fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
// self.derived_property(id, pspec)
|
||||
// }
|
||||
// }
|
||||
|
||||
#[object_subclass]
|
||||
impl ObjectSubclass for TransformPriv {
|
||||
type ParentType = gtk::Bin;
|
||||
type Type = Transform;
|
||||
// #[object_subclass]
|
||||
// impl ObjectSubclass for TransformPriv {
|
||||
// type ParentType = gtk4::Bin;
|
||||
// type Type = Transform;
|
||||
|
||||
const NAME: &'static str = "Transform";
|
||||
// const NAME: &'static str = "Transform";
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.set_css_name("transform");
|
||||
}
|
||||
}
|
||||
// fn class_init(klass: &mut Self::Class) {
|
||||
// klass.set_css_name("transform");
|
||||
// }
|
||||
// }
|
||||
|
||||
impl Default for Transform {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
// impl Default for Transform {
|
||||
// fn default() -> Self {
|
||||
// Self::new()
|
||||
// }
|
||||
// }
|
||||
|
||||
impl Transform {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new::<Self>()
|
||||
}
|
||||
}
|
||||
// impl Transform {
|
||||
// pub fn new() -> Self {
|
||||
// glib::Object::new::<Self>()
|
||||
// }
|
||||
// }
|
||||
|
||||
impl ContainerImpl for TransformPriv {
|
||||
fn add(&self, widget: >k::Widget) {
|
||||
if let Some(content) = &*self.content.borrow() {
|
||||
// TODO: Handle this error when populating children widgets instead
|
||||
error_handling_ctx::print_error(anyhow!(
|
||||
"Error, trying to add multiple children to a circular-progress widget"
|
||||
));
|
||||
self.parent_remove(content);
|
||||
}
|
||||
self.parent_add(widget);
|
||||
self.content.replace(Some(widget.clone()));
|
||||
}
|
||||
}
|
||||
// impl ContainerImpl for TransformPriv {
|
||||
// fn add(&self, widget: >k4::Widget) {
|
||||
// if let Some(content) = &*self.content.borrow() {
|
||||
// // TODO: Handle this error when populating children widgets instead
|
||||
// error_handling_ctx::print_error(anyhow!(
|
||||
// "Error, trying to add multiple children to a circular-progress widget"
|
||||
// ));
|
||||
// self.parent_remove(content);
|
||||
// }
|
||||
// self.parent_add(widget);
|
||||
// self.content.replace(Some(widget.clone()));
|
||||
// }
|
||||
// }
|
||||
|
||||
impl BinImpl for TransformPriv {}
|
||||
impl WidgetImpl for TransformPriv {
|
||||
fn draw(&self, cr: >k::cairo::Context) -> glib::Propagation {
|
||||
let res: Result<()> = (|| {
|
||||
let rotate = *self.rotate.borrow();
|
||||
let total_width = self.obj().allocated_width() as f64;
|
||||
let total_height = self.obj().allocated_height() as f64;
|
||||
// impl BinImpl for TransformPriv {}
|
||||
// impl WidgetImpl for TransformPriv {
|
||||
// fn draw(&self, cr: >k4::cairo::Context) -> glib::Propagation {
|
||||
// let res: Result<()> = (|| {
|
||||
// let rotate = *self.rotate.borrow();
|
||||
// let total_width = self.obj().allocated_width() as f64;
|
||||
// let total_height = self.obj().allocated_height() as f64;
|
||||
|
||||
cr.save()?;
|
||||
// cr.save()?;
|
||||
|
||||
let transform_origin_x = match &*self.transform_origin_x.borrow() {
|
||||
Some(rcx) => {
|
||||
NumWithUnit::from_str(rcx)?.pixels_relative_to(total_width as i32) as f64
|
||||
}
|
||||
None => 0.0,
|
||||
};
|
||||
let transform_origin_y = match &*self.transform_origin_y.borrow() {
|
||||
Some(rcy) => {
|
||||
NumWithUnit::from_str(rcy)?.pixels_relative_to(total_height as i32) as f64
|
||||
}
|
||||
None => 0.0,
|
||||
};
|
||||
// let transform_origin_x = match &*self.transform_origin_x.borrow() {
|
||||
// Some(rcx) => {
|
||||
// NumWithUnit::from_str(rcx)?.pixels_relative_to(total_width as i32) as f64
|
||||
// }
|
||||
// None => 0.0,
|
||||
// };
|
||||
// let transform_origin_y = match &*self.transform_origin_y.borrow() {
|
||||
// Some(rcy) => {
|
||||
// NumWithUnit::from_str(rcy)?.pixels_relative_to(total_height as i32) as f64
|
||||
// }
|
||||
// None => 0.0,
|
||||
// };
|
||||
|
||||
let translate_x = match &*self.translate_x.borrow() {
|
||||
Some(tx) => {
|
||||
NumWithUnit::from_str(tx)?.pixels_relative_to(total_width as i32) as f64
|
||||
}
|
||||
None => 0.0,
|
||||
};
|
||||
// let translate_x = match &*self.translate_x.borrow() {
|
||||
// Some(tx) => {
|
||||
// NumWithUnit::from_str(tx)?.pixels_relative_to(total_width as i32) as f64
|
||||
// }
|
||||
// None => 0.0,
|
||||
// };
|
||||
|
||||
let translate_y = match &*self.translate_y.borrow() {
|
||||
Some(ty) => {
|
||||
NumWithUnit::from_str(ty)?.pixels_relative_to(total_height as i32) as f64
|
||||
}
|
||||
None => 0.0,
|
||||
};
|
||||
// let translate_y = match &*self.translate_y.borrow() {
|
||||
// Some(ty) => {
|
||||
// NumWithUnit::from_str(ty)?.pixels_relative_to(total_height as i32) as f64
|
||||
// }
|
||||
// None => 0.0,
|
||||
// };
|
||||
|
||||
let scale_x = match &*self.scale_x.borrow() {
|
||||
Some(sx) => {
|
||||
NumWithUnit::from_str(sx)?.perc_relative_to(total_width as i32) as f64 / 100.0
|
||||
}
|
||||
None => 1.0,
|
||||
};
|
||||
// let scale_x = match &*self.scale_x.borrow() {
|
||||
// Some(sx) => {
|
||||
// NumWithUnit::from_str(sx)?.perc_relative_to(total_width as i32) as f64 / 100.0
|
||||
// }
|
||||
// None => 1.0,
|
||||
// };
|
||||
|
||||
let scale_y = match &*self.scale_y.borrow() {
|
||||
Some(sy) => {
|
||||
NumWithUnit::from_str(sy)?.perc_relative_to(total_height as i32) as f64 / 100.0
|
||||
}
|
||||
None => 1.0,
|
||||
};
|
||||
// let scale_y = match &*self.scale_y.borrow() {
|
||||
// Some(sy) => {
|
||||
// NumWithUnit::from_str(sy)?.perc_relative_to(total_height as i32) as f64 / 100.0
|
||||
// }
|
||||
// None => 1.0,
|
||||
// };
|
||||
|
||||
cr.translate(transform_origin_x, transform_origin_y);
|
||||
cr.rotate(perc_to_rad(rotate));
|
||||
cr.translate(translate_x - transform_origin_x, translate_y - transform_origin_y);
|
||||
cr.scale(scale_x, scale_y);
|
||||
// cr.translate(transform_origin_x, transform_origin_y);
|
||||
// cr.rotate(perc_to_rad(rotate));
|
||||
// cr.translate(translate_x - transform_origin_x, translate_y - transform_origin_y);
|
||||
// cr.scale(scale_x, scale_y);
|
||||
|
||||
// Children widget
|
||||
if let Some(child) = &*self.content.borrow() {
|
||||
self.obj().propagate_draw(child, cr);
|
||||
}
|
||||
// // Children widget
|
||||
// if let Some(child) = &*self.content.borrow() {
|
||||
// self.obj().propagate_draw(child, cr);
|
||||
// }
|
||||
|
||||
cr.restore()?;
|
||||
Ok(())
|
||||
})();
|
||||
// cr.restore()?;
|
||||
// Ok(())
|
||||
// })();
|
||||
|
||||
if let Err(error) = res {
|
||||
error_handling_ctx::print_error(error)
|
||||
};
|
||||
// if let Err(error) = res {
|
||||
// error_handling_ctx::print_error(error)
|
||||
// };
|
||||
|
||||
glib::Propagation::Proceed
|
||||
}
|
||||
}
|
||||
// glib::Propagation::Proceed
|
||||
// }
|
||||
// }
|
||||
|
||||
fn perc_to_rad(n: f64) -> f64 {
|
||||
(n / 100f64) * 2f64 * std::f64::consts::PI
|
||||
}
|
||||
// fn perc_to_rad(n: f64) -> f64 {
|
||||
// (n / 100f64) * 2f64 * std::f64::consts::PI
|
||||
// }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,24 @@
|
||||
use crate::gtk::prelude::LabelExt;
|
||||
use anyhow::{anyhow, Result};
|
||||
use gtk::pango;
|
||||
use gtk4::glib;
|
||||
use gtk4::glib::gobject_ffi;
|
||||
use gtk4::glib::translate::{FromGlibPtrNone, IntoGlib};
|
||||
use gtk4::glib::Value;
|
||||
use gtk4::pango;
|
||||
use gtk4::prelude::{Cast, ObjectExt, RangeExt, StaticType, ToValue};
|
||||
use rhai::Map;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::widgets::graph::{LineStyle, RenderType};
|
||||
|
||||
// Run a command and get the output
|
||||
pub(super) fn run_command<T>(timeout: std::time::Duration, cmd: &str, args: &[T])
|
||||
where
|
||||
T: 'static + std::fmt::Display + Send + Sync + Clone,
|
||||
{
|
||||
if cmd.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
use wait_timeout::ChildExt;
|
||||
let cmd = replace_placeholders(cmd, args);
|
||||
std::thread::Builder::new()
|
||||
@@ -75,29 +85,29 @@ pub fn dynamic_eq(a: &rhai::Dynamic, b: &rhai::Dynamic) -> bool {
|
||||
}
|
||||
|
||||
/// ALL WIDGETS
|
||||
pub(super) fn parse_align(o: &str) -> Result<gtk::Align> {
|
||||
pub(super) fn parse_align(o: &str) -> Result<gtk4::Align> {
|
||||
match o.to_ascii_lowercase().as_str() {
|
||||
"fill" => Ok(gtk::Align::Fill),
|
||||
"baseline" => Ok(gtk::Align::Baseline),
|
||||
"center" => Ok(gtk::Align::Center),
|
||||
"start" => Ok(gtk::Align::Start),
|
||||
"end" => Ok(gtk::Align::End),
|
||||
"fill" => Ok(gtk4::Align::Fill),
|
||||
"baseline" => Ok(gtk4::Align::Baseline),
|
||||
"center" => Ok(gtk4::Align::Center),
|
||||
"start" => Ok(gtk4::Align::Start),
|
||||
"end" => Ok(gtk4::Align::End),
|
||||
other => Err(anyhow!("Invalid alignment: {}", other)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gtk Box
|
||||
pub(super) fn parse_orientation(ori: &str) -> Result<gtk::Orientation> {
|
||||
pub(super) fn parse_orientation(ori: &str) -> Result<gtk4::Orientation> {
|
||||
match ori.to_ascii_lowercase().as_str() {
|
||||
"h" | "horizontal" => Ok(gtk::Orientation::Horizontal),
|
||||
"v" | "vertical" => Ok(gtk::Orientation::Vertical),
|
||||
"h" | "horizontal" => Ok(gtk4::Orientation::Horizontal),
|
||||
"v" | "vertical" => Ok(gtk4::Orientation::Vertical),
|
||||
other => Err(anyhow!("Invalid orientation: {}", other)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gtk Label
|
||||
pub(super) fn apply_ellipsize_settings(
|
||||
label: >k::Label,
|
||||
label: >k4::Label,
|
||||
truncate: bool,
|
||||
limit_width: i32,
|
||||
truncate_left: bool,
|
||||
@@ -126,12 +136,12 @@ pub(super) fn parse_gravity(s: &str) -> Result<pango::Gravity> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn parse_justification(s: &str) -> Result<gtk::Justification> {
|
||||
pub(super) fn parse_justification(s: &str) -> Result<gtk4::Justification> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"left" => Ok(gtk::Justification::Left),
|
||||
"right" => Ok(gtk::Justification::Right),
|
||||
"center" => Ok(gtk::Justification::Center),
|
||||
"fill" => Ok(gtk::Justification::Fill),
|
||||
"left" => Ok(gtk4::Justification::Left),
|
||||
"right" => Ok(gtk4::Justification::Right),
|
||||
"center" => Ok(gtk4::Justification::Center),
|
||||
"fill" => Ok(gtk4::Justification::Fill),
|
||||
_ => Err(anyhow!("Invalid justification: '{}'", s)),
|
||||
}
|
||||
}
|
||||
@@ -146,12 +156,23 @@ pub(super) fn parse_wrap_mode(s: &str) -> Result<pango::WrapMode> {
|
||||
}
|
||||
|
||||
/// Gtk scale (slider)
|
||||
pub(super) fn parse_position_type(s: &str) -> Result<gtk::PositionType> {
|
||||
pub(super) fn parse_position_type(s: &str) -> Result<gtk4::PositionType> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"left" => Ok(gtk::PositionType::Left),
|
||||
"right" => Ok(gtk::PositionType::Right),
|
||||
"top" => Ok(gtk::PositionType::Top),
|
||||
"bottom" => Ok(gtk::PositionType::Bottom),
|
||||
"left" => Ok(gtk4::PositionType::Left),
|
||||
"right" => Ok(gtk4::PositionType::Right),
|
||||
"top" => Ok(gtk4::PositionType::Top),
|
||||
"bottom" => Ok(gtk4::PositionType::Bottom),
|
||||
_ => Err(anyhow!("Invalid position type: '{}'", s)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gtk flow box
|
||||
pub(super) fn parse_selection_model(s: &str) -> Result<gtk4::SelectionMode> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"none" => Ok(gtk4::SelectionMode::None),
|
||||
"single" => Ok(gtk4::SelectionMode::Single),
|
||||
"browse" => Ok(gtk4::SelectionMode::Browse),
|
||||
"multiple" => Ok(gtk4::SelectionMode::Multiple),
|
||||
_ => Err(anyhow!("Invalid position type: '{}'", s)),
|
||||
}
|
||||
}
|
||||
@@ -172,32 +193,18 @@ where
|
||||
}
|
||||
|
||||
/// Revealer
|
||||
pub(super) fn parse_revealer_transition(t: &str) -> Result<gtk::RevealerTransitionType> {
|
||||
pub(super) fn parse_revealer_transition(t: &str) -> Result<gtk4::RevealerTransitionType> {
|
||||
match t.to_ascii_lowercase().as_str() {
|
||||
"slideright" => Ok(gtk::RevealerTransitionType::SlideRight),
|
||||
"slideleft" => Ok(gtk::RevealerTransitionType::SlideLeft),
|
||||
"slideup" => Ok(gtk::RevealerTransitionType::SlideUp),
|
||||
"slidedown" => Ok(gtk::RevealerTransitionType::SlideDown),
|
||||
"fade" | "crossfade" => Ok(gtk::RevealerTransitionType::Crossfade),
|
||||
"none" => Ok(gtk::RevealerTransitionType::None),
|
||||
"slideright" => Ok(gtk4::RevealerTransitionType::SlideRight),
|
||||
"slideleft" => Ok(gtk4::RevealerTransitionType::SlideLeft),
|
||||
"slideup" => Ok(gtk4::RevealerTransitionType::SlideUp),
|
||||
"slidedown" => Ok(gtk4::RevealerTransitionType::SlideDown),
|
||||
"fade" | "crossfade" => Ok(gtk4::RevealerTransitionType::Crossfade),
|
||||
"none" => Ok(gtk4::RevealerTransitionType::None),
|
||||
_ => Err(anyhow!("Invalid transition: '{}'", t)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gtk Image
|
||||
// icon-size - "menu", "small-toolbar", "toolbar", "large-toolbar", "button", "dnd", "dialog"
|
||||
pub(super) fn parse_icon_size(o: &str) -> Result<gtk::IconSize> {
|
||||
match o.to_ascii_lowercase().as_str() {
|
||||
"menu" => Ok(gtk::IconSize::Menu),
|
||||
"small-toolbar" | "toolbar" => Ok(gtk::IconSize::SmallToolbar),
|
||||
"large-toolbar" => Ok(gtk::IconSize::LargeToolbar),
|
||||
"button" => Ok(gtk::IconSize::Button),
|
||||
"dnd" => Ok(gtk::IconSize::Dnd),
|
||||
"dialog" => Ok(gtk::IconSize::Dialog),
|
||||
_ => Err(anyhow!("Invalid icon size: '{}'", o)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Event box
|
||||
// dragtype - "file", "text"
|
||||
pub(super) enum DragEntryType {
|
||||
@@ -215,14 +222,137 @@ pub(super) fn parse_dragtype(o: &str) -> Result<DragEntryType> {
|
||||
|
||||
/// Stack widget
|
||||
// transition - "slideright", "slideleft", "slideup", "slidedown", "crossfade", "none"
|
||||
pub(super) fn parse_stack_transition(t: &str) -> Result<gtk::StackTransitionType> {
|
||||
pub(super) fn parse_stack_transition(t: &str) -> Result<gtk4::StackTransitionType> {
|
||||
match t.to_ascii_lowercase().as_str() {
|
||||
"slideright" => Ok(gtk::StackTransitionType::SlideRight),
|
||||
"slideleft" => Ok(gtk::StackTransitionType::SlideLeft),
|
||||
"slideup" => Ok(gtk::StackTransitionType::SlideUp),
|
||||
"slidedown" => Ok(gtk::StackTransitionType::SlideDown),
|
||||
"fade" | "crossfade" => Ok(gtk::StackTransitionType::Crossfade),
|
||||
"none" => Ok(gtk::StackTransitionType::None),
|
||||
"slideright" => Ok(gtk4::StackTransitionType::SlideRight),
|
||||
"slideleft" => Ok(gtk4::StackTransitionType::SlideLeft),
|
||||
"slideup" => Ok(gtk4::StackTransitionType::SlideUp),
|
||||
"slidedown" => Ok(gtk4::StackTransitionType::SlideDown),
|
||||
"fade" | "crossfade" => Ok(gtk4::StackTransitionType::Crossfade),
|
||||
"none" => Ok(gtk4::StackTransitionType::None),
|
||||
_ => Err(anyhow!("Invalid stack transition: '{}'", t)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Graph line style
|
||||
pub(super) fn parse_graph_line_style(t: &str) -> Result<LineStyle> {
|
||||
match t.to_ascii_lowercase().as_str() {
|
||||
"miter" => Ok(LineStyle::Miter),
|
||||
"bevel" => Ok(LineStyle::Bevel),
|
||||
"round" => Ok(LineStyle::Round),
|
||||
_ => Err(anyhow!("Invalid graph line style: '{}'", t)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Graph render type
|
||||
pub(super) fn parse_graph_render_type(t: &str) -> Result<RenderType> {
|
||||
match t.to_ascii_lowercase().as_str() {
|
||||
"line" => Ok(RenderType::Line),
|
||||
"step-line" => Ok(RenderType::StepLine),
|
||||
"fill" => Ok(RenderType::Fill),
|
||||
"step-fill" => Ok(RenderType::StepFill),
|
||||
_ => Err(anyhow!("Invalid graph render type: '{}'", t)),
|
||||
}
|
||||
}
|
||||
|
||||
// For localbind
|
||||
pub(super) fn set_property_from_string_anywhere(
|
||||
widget: >k4::Widget,
|
||||
prop_name: &str,
|
||||
value_str: &str,
|
||||
) {
|
||||
fn convert(pspec: &glib::ParamSpec, value_str: &str) -> Option<Value> {
|
||||
let value_type = pspec.value_type();
|
||||
|
||||
if value_type == f64::static_type() {
|
||||
value_str.parse::<f64>().ok().map(|v| v.to_value())
|
||||
} else if value_type == i32::static_type() {
|
||||
value_str.parse::<i32>().ok().map(|v| v.to_value())
|
||||
} else if value_type == bool::static_type() {
|
||||
value_str.parse::<bool>().ok().map(|v| v.to_value())
|
||||
} else if value_type == String::static_type() {
|
||||
Some(value_str.to_value())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
let obj: &glib::Object = widget.upcast_ref();
|
||||
|
||||
if let Some(pspec) = obj.find_property(prop_name) {
|
||||
if let Some(gv) = convert(&pspec, value_str) {
|
||||
obj.set_property(prop_name, &gv);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
for iface_type in obj.type_().interfaces() {
|
||||
for pspec in list_interface_properties(iface_type) {
|
||||
if pspec.name() == prop_name {
|
||||
if let Some(v) = convert(&pspec, value_str) {
|
||||
obj.set_property(prop_name, &v);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(range) = widget.downcast_ref::<gtk4::Range>() {
|
||||
let range_obj: &glib::Object = range.upcast_ref();
|
||||
|
||||
if let Some(pspec) = range_obj.find_property(prop_name) {
|
||||
if let Some(gv) = convert(&pspec, value_str) {
|
||||
range_obj.set_property(prop_name, &gv);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let adj = range.adjustment();
|
||||
let adj_obj: &glib::Object = adj.upcast_ref();
|
||||
|
||||
if let Some(pspec) = adj_obj.find_property(prop_name) {
|
||||
if let Some(gv) = convert(&pspec, value_str) {
|
||||
adj_obj.set_property(prop_name, &gv);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
log::error!("Property '{}' not found on widget {}", prop_name, obj.type_().name());
|
||||
}
|
||||
|
||||
unsafe fn list_interface_properties(iface_type: glib::Type) -> Vec<glib::ParamSpec> {
|
||||
let mut n_props = 0;
|
||||
|
||||
let iface_ptr = gobject_ffi::g_type_default_interface_ref(iface_type.into_glib());
|
||||
if iface_ptr.is_null() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let props_ptr =
|
||||
gobject_ffi::g_object_interface_list_properties(iface_ptr as *mut _, &mut n_props);
|
||||
|
||||
let props = (0..n_props)
|
||||
.map(|i| {
|
||||
let p = *props_ptr.add(i as usize);
|
||||
glib::ParamSpec::from_glib_none(p)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
gobject_ffi::g_type_default_interface_unref(iface_ptr);
|
||||
|
||||
props
|
||||
}
|
||||
|
||||
/// Picture widget
|
||||
pub(super) fn parse_content_fit(cf: &str) -> Result<gtk4::ContentFit> {
|
||||
match cf.to_ascii_lowercase().as_str() {
|
||||
"fill" => Ok(gtk4::ContentFit::Fill),
|
||||
"contain" => Ok(gtk4::ContentFit::Contain),
|
||||
"cover" => Ok(gtk4::ContentFit::Cover),
|
||||
"scaledown" => Ok(gtk4::ContentFit::ScaleDown),
|
||||
_ => Err(anyhow!("Invalid content fit: '{}'", cf)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
use gtk::glib::{self, object_subclass, wrapper, Properties};
|
||||
use gtk::{prelude::*, subclass::prelude::*};
|
||||
use std::cell::RefCell;
|
||||
|
||||
wrapper! {
|
||||
pub struct Window(ObjectSubclass<WindowPriv>)
|
||||
@extends gtk::Window, gtk::Bin, gtk::Container, gtk::Widget, @implements gtk::Buildable;
|
||||
}
|
||||
|
||||
#[derive(Properties)]
|
||||
#[properties(wrapper_type = Window)]
|
||||
pub struct WindowPriv {
|
||||
#[property(get, name = "x", nick = "X", blurb = "Global x coordinate", default = 0)]
|
||||
x: RefCell<i32>,
|
||||
|
||||
#[property(get, name = "y", nick = "Y", blurb = "Global y coordinate", default = 0)]
|
||||
y: RefCell<i32>,
|
||||
}
|
||||
|
||||
// This should match the default values from the ParamSpecs
|
||||
impl Default for WindowPriv {
|
||||
fn default() -> Self {
|
||||
WindowPriv { x: RefCell::new(0), y: RefCell::new(0) }
|
||||
}
|
||||
}
|
||||
|
||||
#[object_subclass]
|
||||
impl ObjectSubclass for WindowPriv {
|
||||
type ParentType = gtk::Window;
|
||||
type Type = Window;
|
||||
|
||||
const NAME: &'static str = "WindowEwwii";
|
||||
}
|
||||
|
||||
impl Default for Window {
|
||||
fn default() -> Self {
|
||||
glib::Object::new::<Self>()
|
||||
}
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn new(type_: gtk::WindowType, x_: i32, y_: i32) -> Self {
|
||||
let w: Self = glib::Object::builder().property("type", type_).build();
|
||||
let priv_ = w.imp();
|
||||
priv_.x.replace(x_);
|
||||
priv_.y.replace(y_);
|
||||
w
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for WindowPriv {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
Self::derived_properties()
|
||||
}
|
||||
|
||||
fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
self.derived_property(id, pspec)
|
||||
}
|
||||
}
|
||||
impl WindowImpl for WindowPriv {}
|
||||
impl BinImpl for WindowPriv {}
|
||||
impl ContainerImpl for WindowPriv {}
|
||||
impl WidgetImpl for WindowPriv {}
|
||||
@@ -33,12 +33,13 @@ impl NumWithUnit {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn perc_relative_to(&self, max: i32) -> f32 {
|
||||
match *self {
|
||||
NumWithUnit::Percent(n) => n,
|
||||
NumWithUnit::Pixels(n) => ((n as f64 / max as f64) * 100.0) as f32,
|
||||
}
|
||||
}
|
||||
// add back when needed
|
||||
// pub fn perc_relative_to(&self, max: i32) -> f32 {
|
||||
// match *self {
|
||||
// NumWithUnit::Percent(n) => n,
|
||||
// NumWithUnit::Pixels(n) => ((n as f64 / max as f64) * 100.0) as f32,
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
impl FromStr for NumWithUnit {
|
||||
|
||||
23
crates/ewwii_plugin_api/Cargo.toml
Normal file
23
crates/ewwii_plugin_api/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "ewwii_plugin_api"
|
||||
version = "0.7.0"
|
||||
authors = ["byson94 <byson94wastaken@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
description = "A shared library for building plugins for ewwii"
|
||||
repository = "https://github.com/byson94/ewwii"
|
||||
homepage = "https://github.com/byson94/ewwii"
|
||||
|
||||
[features]
|
||||
default = ["include-gtk4", "include-rhai"]
|
||||
|
||||
## Include the gtk4 dependency
|
||||
include-gtk4 = ["dep:gtk4"]
|
||||
|
||||
## Include the rhai dependency
|
||||
include-rhai = ["dep:rhai"]
|
||||
|
||||
[dependencies]
|
||||
# rhai crate features should exactly match that of rhai_impl
|
||||
rhai = { workspace = true, optional = true, features = ["internals"] }
|
||||
gtk4 = { workspace = true, optional = true }
|
||||
28
crates/ewwii_plugin_api/README.md
Normal file
28
crates/ewwii_plugin_api/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# ewwii_plugin_api
|
||||
|
||||
A shared interface providing traits for building plugins for ewwii.
|
||||
|
||||
## Example
|
||||
|
||||
A simple example showing how to use this interface is provided below:
|
||||
|
||||
```rust
|
||||
use ewwii_plugin_api::{EwwiiAPI, Plugin, export_plugin};
|
||||
|
||||
pub struct DummyStructure;
|
||||
|
||||
impl Plugin for DummyStructure {
|
||||
// critical for ewwii to launch the plugin
|
||||
fn init(&self, host: &dyn EwwiiAPI) {
|
||||
// will be printed by the host
|
||||
host.log("Plugin says Hello!");
|
||||
host.rhai_engine_action(Box::new(|engine| {
|
||||
let ast = engine.compile("1+1");
|
||||
println!("Compiled AST: {:#?}", ast);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Critical for ewwii to see the plugin
|
||||
export_plugin!(DummyStructure);
|
||||
```
|
||||
11
crates/ewwii_plugin_api/src/example.rs
Normal file
11
crates/ewwii_plugin_api/src/example.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
//! A module providing example implementations of plugins
|
||||
|
||||
/// An example plugin that can be exported directly
|
||||
pub struct ExamplePlugin;
|
||||
|
||||
impl crate::Plugin for ExamplePlugin {
|
||||
/// Example code that initalizes the plugin
|
||||
fn init(&self, host: &dyn crate::EwwiiAPI) {
|
||||
host.log("Example plugin says Hello!");
|
||||
}
|
||||
}
|
||||
63
crates/ewwii_plugin_api/src/export_macros.rs
Normal file
63
crates/ewwii_plugin_api/src/export_macros.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
//! Module implementing macros
|
||||
|
||||
/// Macro to implement and export a plugin in a single step.
|
||||
/// With this macro, users can write their plugin code directly
|
||||
/// without having to manually implement each trait.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// The following example shows how you can use this macro to
|
||||
/// easily make plugins in a single step.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use ewwii_plugin_api::auto_plugin;
|
||||
///
|
||||
/// auto_plugin!(MyPluginName, {
|
||||
/// // host variable is passed in automatically
|
||||
/// host.log("Easy, huh?");
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// ## When not to use it
|
||||
///
|
||||
/// This macro shall not be used if you prefer flexibility and safety.
|
||||
/// The manual approach is verbose, but is way safer and flexible than using this macro.
|
||||
#[macro_export]
|
||||
macro_rules! auto_plugin {
|
||||
($struct_name:ident, $init_block:block) => {
|
||||
pub struct $struct_name;
|
||||
|
||||
// Implement the Plugin trait
|
||||
impl $crate::Plugin for $struct_name {
|
||||
fn init(&self, host: &dyn $crate::EwwiiAPI) {
|
||||
$init_block
|
||||
}
|
||||
}
|
||||
|
||||
$crate::export_plugin!($struct_name);
|
||||
};
|
||||
}
|
||||
|
||||
/// Automatically implements `create_plugin` for a given fieldless structure
|
||||
#[macro_export]
|
||||
macro_rules! export_plugin {
|
||||
($plugin_struct:path) => {
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn create_plugin() -> Box<dyn $crate::Plugin> {
|
||||
Box::new($plugin_struct)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Automatically implements `create_plugin` for a given structure that has fields.
|
||||
///
|
||||
/// This macro expects the structure to have fields and also implement a `default()` method.
|
||||
#[macro_export]
|
||||
macro_rules! export_stateful_plugin {
|
||||
($plugin_struct:path) => {
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn create_plugin() -> Box<dyn $crate::Plugin> {
|
||||
Box::new(<$plugin_struct>::default())
|
||||
}
|
||||
};
|
||||
}
|
||||
180
crates/ewwii_plugin_api/src/lib.rs
Normal file
180
crates/ewwii_plugin_api/src/lib.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
//! # ewwii_plugin_api - A plugin interface for ewwii
|
||||
//!
|
||||
//! `ewwii_plguin_api` is a shared list of traits
|
||||
//! that both ewwii and its plugins can use.
|
||||
//! This crate simplifies and provides a safe way for building
|
||||
//! plugins for ewwii.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! The following example shows how this crate shall be used to build ewwii plugins:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ewwii_plugin_api::{EwwiiAPI, Plugin, export_plugin};
|
||||
//!
|
||||
//! pub struct DummyStructure;
|
||||
//!
|
||||
//! impl Plugin for DummyStructure {
|
||||
//! // critical for ewwii to launch the plugin
|
||||
//! fn init(&self, host: &dyn EwwiiAPI) {
|
||||
//! // will be printed by the host
|
||||
//! host.log("Plugin says Hello!");
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! // Critical for ewwii to load the plugin
|
||||
//! export_plugin!(DummyStructure);
|
||||
//! ```
|
||||
|
||||
mod export_macros;
|
||||
|
||||
pub mod example;
|
||||
pub mod rhai_backend;
|
||||
pub mod widget_backend;
|
||||
|
||||
#[cfg(feature = "include-rhai")]
|
||||
pub use rhai;
|
||||
|
||||
#[cfg(feature = "include-gtk4")]
|
||||
pub use gtk4;
|
||||
|
||||
/// The shared trait defining the Ewwii plugin API
|
||||
pub trait EwwiiAPI: Send + Sync {
|
||||
// == General Stuff == //
|
||||
/// Print a message from the host
|
||||
fn print(&self, msg: &str);
|
||||
/// Log a message from the host
|
||||
fn log(&self, msg: &str);
|
||||
/// Log a warning from the host
|
||||
fn warn(&self, msg: &str);
|
||||
/// Log an error from the host
|
||||
fn error(&self, msg: &str);
|
||||
|
||||
// == Rhai Manipulation Stuff == //
|
||||
/// _(include-rhai)_ Perform actions on the latest rhai engine.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ewwii_plugin_api::{EwwiiAPI, Plugin};
|
||||
///
|
||||
/// pub struct DummyStructure;
|
||||
///
|
||||
/// impl Plugin for DummyStructure {
|
||||
/// fn init(&self, host: &dyn EwwiiAPI) {
|
||||
/// host.rhai_engine_action(Box::new(|eng| {
|
||||
/// // eng = rhai::Engine
|
||||
/// eng.set_max_expr_depths(128, 128);
|
||||
/// }));
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(feature = "include-rhai")]
|
||||
fn rhai_engine_action(
|
||||
&self,
|
||||
f: Box<dyn FnOnce(&mut rhai::Engine) + Send>,
|
||||
) -> Result<(), String>;
|
||||
|
||||
/// _(include-rhai)_ Expose a function that rhai configuration can call.
|
||||
///
|
||||
/// **NOTE:***
|
||||
///
|
||||
/// Due to TypeID mismatches, methods like `register_type`, `register_fn`,
|
||||
/// etc. won't work on the engine and may cause a crash. It is recommended
|
||||
/// to use the `register_function` API to register a funtion which `api::slib`
|
||||
/// can call to in rhai.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ewwii_plugin_api::{EwwiiAPI, Plugin, rhai_backend::RhaiFnNamespace};
|
||||
/// use rhai::Dynamic;
|
||||
///
|
||||
/// pub struct DummyStructure;
|
||||
///
|
||||
/// impl Plugin for DummyStructure {
|
||||
/// fn init(&self, host: &dyn EwwiiAPI) {
|
||||
/// host.register_function(
|
||||
/// "my_func".to_string(),
|
||||
/// RhaiFnNamespace::Global,
|
||||
/// Box::new(|args| {
|
||||
/// // Do stuff
|
||||
/// // - Perform things on the args (if needed)
|
||||
/// // - And return a value
|
||||
///
|
||||
/// Ok(Dynamic::default()) // return empty
|
||||
/// }));
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This example will register a function with signature "my_func(Array)" in rhai.
|
||||
///
|
||||
/// ## Example use in rhai
|
||||
///
|
||||
/// ```js
|
||||
/// print(my_func(["param1", "param2"]));
|
||||
/// ```
|
||||
#[cfg(feature = "include-rhai")]
|
||||
fn register_function(
|
||||
&self,
|
||||
name: String,
|
||||
namespace: rhai_backend::RhaiFnNamespace,
|
||||
f: Box<
|
||||
dyn Fn(rhai::Array) -> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> + Send + Sync,
|
||||
>,
|
||||
) -> Result<(), String>;
|
||||
|
||||
// == Widget Rendering & Logic == //
|
||||
/// Get the list of all widget id's
|
||||
fn list_widget_ids(&self) -> Result<Vec<u64>, String>;
|
||||
|
||||
/// _(include-gtk4)_ Perform actions on the latest widget registry.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ewwii_plugin_api::{EwwiiAPI, Plugin};
|
||||
///
|
||||
/// pub struct DummyStructure;
|
||||
///
|
||||
/// impl Plugin for DummyStructure {
|
||||
/// fn init(&self, host: &dyn EwwiiAPI) {
|
||||
/// host.widget_reg_action(Box::new(|wrg| {
|
||||
/// // wrg = widget_backend::WidgetRegistryRepr
|
||||
/// // The gtk4::Widget can be modified here.
|
||||
/// }));
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(feature = "include-gtk4")]
|
||||
fn widget_reg_action(
|
||||
&self,
|
||||
f: Box<dyn FnOnce(&mut widget_backend::WidgetRegistryRepr) + Send>,
|
||||
) -> Result<(), String>;
|
||||
}
|
||||
|
||||
/// The API format that the plugin should follow.
|
||||
/// This trait should be implemented for a structure and
|
||||
/// that structure should be exported via FFI.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ewwii_plugin_api::{Plugin, EwwiiAPI, export_plugin};
|
||||
///
|
||||
/// struct MyStruct;
|
||||
///
|
||||
/// impl Plugin for MyStruct {
|
||||
/// fn init(&self, host: &dyn EwwiiAPI) {
|
||||
/// /* Implementation Skipped */
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Automatically does all the FFI related exports
|
||||
/// export_plugin!(MyStruct);
|
||||
/// ```
|
||||
pub trait Plugin: Send + Sync {
|
||||
/// Function ran by host to startup plugin (and its a must-have for plugin loading)
|
||||
fn init(&self, host: &dyn EwwiiAPI);
|
||||
}
|
||||
14
crates/ewwii_plugin_api/src/rhai_backend.rs
Normal file
14
crates/ewwii_plugin_api/src/rhai_backend.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
//! Module exposing extra utilities for rhai.
|
||||
|
||||
#[cfg(feature = "include-rhai")]
|
||||
mod rhai_included {
|
||||
/// _(include-rhai)_ An enumrate providing options for
|
||||
/// function registaration namespaces.
|
||||
pub enum RhaiFnNamespace {
|
||||
Custom(String),
|
||||
Global,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "include-rhai")]
|
||||
pub use rhai_included::*;
|
||||
21
crates/ewwii_plugin_api/src/widget_backend.rs
Normal file
21
crates/ewwii_plugin_api/src/widget_backend.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
//! Module exposing structures and types from the
|
||||
//! Widget rendering and definition backend in ewwii.
|
||||
|
||||
#[cfg(feature = "include-gtk4")]
|
||||
mod gtk4_included {
|
||||
use gtk4::Widget as GtkWidget;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// _(include-gtk4)_ A representation of widget registry which holds all the
|
||||
/// information needed for the dynamic runtime engine in ewwii.
|
||||
///
|
||||
/// Not every change in this structure will be represented in the
|
||||
/// original WidgetRegistry in ewwii. Only the change on gtk4::Widget
|
||||
/// is reflected back.
|
||||
pub struct WidgetRegistryRepr<'a> {
|
||||
pub widgets: HashMap<u64, &'a mut GtkWidget>,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "include-gtk4")]
|
||||
pub use gtk4_included::*;
|
||||
@@ -10,10 +10,10 @@ homepage = "https://github.com/byson94/ewwii"
|
||||
|
||||
[dependencies]
|
||||
shared_utils.workspace = true
|
||||
scan_prop_proc.workspace = true
|
||||
|
||||
rhai = { workspace = true, features = ["internals"] }
|
||||
anyhow.workspace = true
|
||||
gtk.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
log.workspace = true
|
||||
once_cell.workspace = true
|
||||
@@ -22,6 +22,7 @@ ahash.workspace = true
|
||||
nix = { workspace = true, features = ["process", "fs", "signal"] }
|
||||
libc.workspace = true
|
||||
# error handling
|
||||
rhai_trace = "0.3.0"
|
||||
rhai_trace = "0.3.1"
|
||||
codespan-reporting.workspace = true
|
||||
regex.workspace = true
|
||||
regex.workspace = true
|
||||
gtk4.workspace = true
|
||||
@@ -1,20 +1,22 @@
|
||||
use ahash::AHasher;
|
||||
use anyhow::Result;
|
||||
use rhai::Map;
|
||||
use scan_prop_proc::scan_prop;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[scan_prop]
|
||||
pub enum WidgetNode {
|
||||
Label { props: Map },
|
||||
Box { props: Map, children: Vec<WidgetNode> },
|
||||
CenterBox { props: Map, children: Vec<WidgetNode> },
|
||||
FlowBox { props: Map, children: Vec<WidgetNode> },
|
||||
Button { props: Map },
|
||||
Image { props: Map },
|
||||
Input { props: Map },
|
||||
Progress { props: Map },
|
||||
ComboBoxText { props: Map },
|
||||
Slider { props: Map },
|
||||
Scale { props: Map },
|
||||
Checkbox { props: Map },
|
||||
Expander { props: Map, children: Vec<WidgetNode> },
|
||||
Revealer { props: Map, children: Vec<WidgetNode> },
|
||||
@@ -30,6 +32,11 @@ pub enum WidgetNode {
|
||||
EventBox { props: Map, children: Vec<WidgetNode> },
|
||||
ToolTip { props: Map, children: Vec<WidgetNode> },
|
||||
|
||||
// Special
|
||||
LocalBind { props: Map, children: Vec<WidgetNode> },
|
||||
WidgetAction { props: Map, children: Vec<WidgetNode> },
|
||||
GtkUI { props: Map },
|
||||
|
||||
// Top-level macros
|
||||
DefWindow { name: String, props: Map, node: Box<WidgetNode> },
|
||||
// Poll { var: String, interval: String, cmd: String, initial: String },
|
||||
@@ -72,16 +79,9 @@ pub fn get_id_to_widget_info<'a>(
|
||||
get_id_to_widget_info(child, id_to_props, Some(id))?;
|
||||
}
|
||||
}
|
||||
WidgetNode::CenterBox { props, children } => {
|
||||
let id = hash_props_and_type(props, "CenterBox");
|
||||
insert_wdgt_info(
|
||||
node,
|
||||
props,
|
||||
"CenterBox",
|
||||
children.as_slice(),
|
||||
parent_id,
|
||||
id_to_props,
|
||||
)?;
|
||||
WidgetNode::FlowBox { props, children } => {
|
||||
let id = hash_props_and_type(props, "FlowBox");
|
||||
insert_wdgt_info(node, props, "FlowBox", children.as_slice(), parent_id, id_to_props)?;
|
||||
for child in children {
|
||||
get_id_to_widget_info(child, id_to_props, Some(id))?;
|
||||
}
|
||||
@@ -105,9 +105,9 @@ pub fn get_id_to_widget_info<'a>(
|
||||
// let id = hash_props_and_type(props, "Transform");
|
||||
insert_wdgt_info(node, props, "Transform", &[], parent_id, id_to_props)?;
|
||||
}
|
||||
WidgetNode::Slider { props } => {
|
||||
// let id = hash_props_and_type(props, "Slider");
|
||||
insert_wdgt_info(node, props, "Slider", &[], parent_id, id_to_props)?;
|
||||
WidgetNode::Scale { props } => {
|
||||
// let id = hash_props_and_type(props, "Scale");
|
||||
insert_wdgt_info(node, props, "Scale", &[], parent_id, id_to_props)?;
|
||||
}
|
||||
WidgetNode::Progress { props } => {
|
||||
// let id = hash_props_and_type(props, "Progress");
|
||||
@@ -151,6 +151,37 @@ pub fn get_id_to_widget_info<'a>(
|
||||
get_id_to_widget_info(child, id_to_props, Some(id))?;
|
||||
}
|
||||
}
|
||||
WidgetNode::LocalBind { props, children } => {
|
||||
let id = hash_props_and_type(props, "LocalBind");
|
||||
insert_wdgt_info(
|
||||
node,
|
||||
props,
|
||||
"LocalBind",
|
||||
children.as_slice(),
|
||||
parent_id,
|
||||
id_to_props,
|
||||
)?;
|
||||
for child in children {
|
||||
get_id_to_widget_info(child, id_to_props, Some(id))?;
|
||||
}
|
||||
}
|
||||
WidgetNode::WidgetAction { props, children } => {
|
||||
let id = hash_props_and_type(props, "WidgetAction");
|
||||
insert_wdgt_info(
|
||||
node,
|
||||
props,
|
||||
"WidgetAction",
|
||||
children.as_slice(),
|
||||
parent_id,
|
||||
id_to_props,
|
||||
)?;
|
||||
for child in children {
|
||||
get_id_to_widget_info(child, id_to_props, Some(id))?;
|
||||
}
|
||||
}
|
||||
WidgetNode::GtkUI { props } => {
|
||||
insert_wdgt_info(node, props, "GtkUI", &[], parent_id, id_to_props)?;
|
||||
}
|
||||
WidgetNode::ColorChooser { props } => {
|
||||
// let id = hash_props_and_type(props, "ColorChooser");
|
||||
insert_wdgt_info(node, props, "ColorChooser", &[], parent_id, id_to_props)?;
|
||||
@@ -227,6 +258,14 @@ pub fn hash_props_and_type(props: &Map, widget_type_str: &str) -> u64 {
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
pub fn hash_props(props: &Map) -> u64 {
|
||||
let mut hasher = AHasher::default();
|
||||
|
||||
props.hash(&mut hasher);
|
||||
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::ast::WidgetNode;
|
||||
use crate::ast::{hash_props, WidgetNode};
|
||||
use crate::updates::{register_signal, LocalDataBinder, LocalSignal};
|
||||
use rhai::{Array, Engine, EvalAltResult, Map, NativeCallContext};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Converts a Dynamic array into a Vec<WidgetNode>, returning proper errors with position.
|
||||
fn children_to_vec(
|
||||
@@ -22,8 +24,13 @@ fn children_to_vec(
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn register_all_widgets(engine: &mut Engine, all_nodes: &Rc<RefCell<Vec<WidgetNode>>>) {
|
||||
pub fn register_all_widgets(
|
||||
engine: &mut Engine,
|
||||
all_nodes: &Rc<RefCell<Vec<WidgetNode>>>,
|
||||
keep_signal: &Rc<RefCell<Vec<u64>>>,
|
||||
) {
|
||||
engine.register_type::<WidgetNode>();
|
||||
engine.register_type::<LocalSignal>();
|
||||
|
||||
// == Primitive widgets ==
|
||||
macro_rules! register_primitive {
|
||||
@@ -40,7 +47,7 @@ pub fn register_all_widgets(engine: &mut Engine, all_nodes: &Rc<RefCell<Vec<Widg
|
||||
register_primitive!("input", Input);
|
||||
register_primitive!("progress", Progress);
|
||||
register_primitive!("combo_box_text", ComboBoxText);
|
||||
register_primitive!("slider", Slider);
|
||||
register_primitive!("scale", Scale);
|
||||
register_primitive!("checkbox", Checkbox);
|
||||
register_primitive!("calendar", Calendar);
|
||||
register_primitive!("graph", Graph);
|
||||
@@ -66,7 +73,7 @@ pub fn register_all_widgets(engine: &mut Engine, all_nodes: &Rc<RefCell<Vec<Widg
|
||||
}
|
||||
|
||||
register_with_children!("box", Box);
|
||||
register_with_children!("centerbox", CenterBox);
|
||||
register_with_children!("flowbox", FlowBox);
|
||||
register_with_children!("expander", Expander);
|
||||
register_with_children!("revealer", Revealer);
|
||||
register_with_children!("scroll", Scroll);
|
||||
@@ -74,6 +81,35 @@ pub fn register_all_widgets(engine: &mut Engine, all_nodes: &Rc<RefCell<Vec<Widg
|
||||
register_with_children!("stack", Stack);
|
||||
register_with_children!("eventbox", EventBox);
|
||||
register_with_children!("tooltip", ToolTip);
|
||||
register_with_children!("localbind", LocalBind);
|
||||
register_with_children!("widget_action", WidgetAction);
|
||||
|
||||
// == Special widget
|
||||
engine.register_fn(
|
||||
"gtk_ui",
|
||||
|path: &str, load: &str| -> Result<WidgetNode, Box<EvalAltResult>> {
|
||||
let mut props = Map::new();
|
||||
props.insert("file".into(), path.into());
|
||||
props.insert("id".into(), load.into());
|
||||
Ok(WidgetNode::GtkUI { props })
|
||||
},
|
||||
);
|
||||
|
||||
// == Special signal
|
||||
let keep_signal_clone = keep_signal.clone();
|
||||
engine.register_fn(
|
||||
"localsignal",
|
||||
move |props: Map| -> Result<LocalSignal, Box<EvalAltResult>> {
|
||||
let id = hash_props(&props);
|
||||
let signal = Rc::new(LocalSignal { id, props, data: Arc::new(LocalDataBinder::new()) });
|
||||
|
||||
let signal_rc = register_signal(id, signal);
|
||||
|
||||
keep_signal_clone.borrow_mut().push(id);
|
||||
|
||||
Ok((*signal_rc).clone())
|
||||
},
|
||||
);
|
||||
|
||||
// == Top-level macros ==
|
||||
engine.register_fn(
|
||||
|
||||
@@ -39,9 +39,9 @@ impl WidgetNode {
|
||||
props: with_dyn_id(props.clone(), parent_path),
|
||||
children: process_children(children, parent_path, "box"),
|
||||
},
|
||||
WidgetNode::CenterBox { props, children } => WidgetNode::CenterBox {
|
||||
WidgetNode::FlowBox { props, children } => WidgetNode::FlowBox {
|
||||
props: with_dyn_id(props.clone(), parent_path),
|
||||
children: process_children(children, parent_path, "centerbox"),
|
||||
children: process_children(children, parent_path, "flowbox"),
|
||||
},
|
||||
WidgetNode::Expander { props, children } => WidgetNode::Expander {
|
||||
props: with_dyn_id(props.clone(), parent_path),
|
||||
@@ -71,6 +71,14 @@ impl WidgetNode {
|
||||
props: with_dyn_id(props.clone(), parent_path),
|
||||
children: process_children(children, parent_path, "tooltip"),
|
||||
},
|
||||
WidgetNode::LocalBind { props, children } => WidgetNode::LocalBind {
|
||||
props: with_dyn_id(props.clone(), parent_path),
|
||||
children: process_children(children, parent_path, "localbind"),
|
||||
},
|
||||
WidgetNode::WidgetAction { props, children } => WidgetNode::WidgetAction {
|
||||
props: with_dyn_id(props.clone(), parent_path),
|
||||
children: process_children(children, parent_path, "widget_action"),
|
||||
},
|
||||
|
||||
// == Top-level container for multiple widgets ==
|
||||
WidgetNode::Enter(children) => {
|
||||
@@ -94,13 +102,14 @@ impl WidgetNode {
|
||||
| node @ WidgetNode::Input { props }
|
||||
| node @ WidgetNode::Progress { props }
|
||||
| node @ WidgetNode::ComboBoxText { props }
|
||||
| node @ WidgetNode::Slider { props }
|
||||
| node @ WidgetNode::Scale { props }
|
||||
| node @ WidgetNode::Checkbox { props }
|
||||
| node @ WidgetNode::Calendar { props }
|
||||
| node @ WidgetNode::ColorButton { props }
|
||||
| node @ WidgetNode::ColorChooser { props }
|
||||
| node @ WidgetNode::CircularProgress { props }
|
||||
| node @ WidgetNode::Graph { props }
|
||||
| node @ WidgetNode::GtkUI { props }
|
||||
| node @ WidgetNode::Transform { props } => {
|
||||
let new_props = with_dyn_id(props.clone(), parent_path);
|
||||
match node {
|
||||
@@ -112,7 +121,7 @@ impl WidgetNode {
|
||||
WidgetNode::ComboBoxText { .. } => {
|
||||
WidgetNode::ComboBoxText { props: new_props }
|
||||
}
|
||||
WidgetNode::Slider { .. } => WidgetNode::Slider { props: new_props },
|
||||
WidgetNode::Scale { .. } => WidgetNode::Scale { props: new_props },
|
||||
WidgetNode::Checkbox { .. } => WidgetNode::Checkbox { props: new_props },
|
||||
WidgetNode::Calendar { .. } => WidgetNode::Calendar { props: new_props },
|
||||
WidgetNode::ColorButton { .. } => WidgetNode::ColorButton { props: new_props },
|
||||
@@ -123,6 +132,7 @@ impl WidgetNode {
|
||||
WidgetNode::CircularProgress { props: new_props }
|
||||
}
|
||||
WidgetNode::Graph { .. } => WidgetNode::Graph { props: new_props },
|
||||
WidgetNode::GtkUI { .. } => WidgetNode::GtkUI { props: new_props },
|
||||
WidgetNode::Transform { .. } => WidgetNode::Transform { props: new_props },
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
@@ -11,9 +11,15 @@ pub fn format_eval_error(
|
||||
engine: &Engine,
|
||||
file_id: Option<&str>,
|
||||
) -> String {
|
||||
let error_str = error.to_string();
|
||||
|
||||
if error_str == "" || error_str == "module_eval_failed" || error_str == "module_parse_failed" {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let better_error =
|
||||
BetterError::improve_eval_error(error, code, engine, None).unwrap_or(BetterError {
|
||||
message: error.to_string(),
|
||||
message: error_str,
|
||||
help: None,
|
||||
hint: None,
|
||||
note: None,
|
||||
@@ -24,8 +30,14 @@ pub fn format_eval_error(
|
||||
|
||||
/// Return a formatted Rhai parse error.
|
||||
pub fn format_parse_error(error: &ParseError, code: &str, file_id: Option<&str>) -> String {
|
||||
let error_str = error.to_string();
|
||||
|
||||
if error_str == "" || error_str == "module_eval_failed" || error_str == "module_parse_failed" {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let better_error = BetterError::improve_parse_error(error, code).unwrap_or(BetterError {
|
||||
message: error.to_string(),
|
||||
message: error_str,
|
||||
help: None,
|
||||
hint: None,
|
||||
note: None,
|
||||
@@ -52,12 +64,15 @@ pub fn format_codespan_error(be: BetterError, code: &str, file_id: Option<&str>)
|
||||
}
|
||||
|
||||
// build the diagnostic error
|
||||
let diagnostic = Diagnostic::error()
|
||||
.with_message(&be.message)
|
||||
.with_labels(vec![
|
||||
Label::primary(file_id, be.span.start()..be.span.end()).with_message(&be.message)
|
||||
])
|
||||
.with_notes(notes);
|
||||
let mut labels = Vec::new();
|
||||
if be.span.start() != be.span.end() {
|
||||
labels.push(
|
||||
Label::primary(file_id, be.span.start()..be.span.end()).with_message(&be.message),
|
||||
);
|
||||
}
|
||||
|
||||
let diagnostic =
|
||||
Diagnostic::error().with_message(&be.message).with_labels(labels).with_notes(notes);
|
||||
|
||||
let mut buffer = Buffer::ansi();
|
||||
let config = term::Config::default();
|
||||
|
||||
@@ -1,13 +1,44 @@
|
||||
use crate::error::format_eval_error;
|
||||
use anyhow::Result;
|
||||
use rhai::Engine;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub fn extract_poll_and_listen_vars(code: &str) -> Result<Vec<(String, Option<String>)>> {
|
||||
extract_poll_and_listen_vars_inner(code, &mut HashSet::new())
|
||||
}
|
||||
|
||||
fn extract_poll_and_listen_vars_inner(
|
||||
code: &str,
|
||||
visited: &mut HashSet<PathBuf>,
|
||||
) -> Result<Vec<(String, Option<String>)>> {
|
||||
let mut results = Vec::new();
|
||||
let mut engine = Engine::new();
|
||||
|
||||
register_temp_poll_listen(&mut engine);
|
||||
|
||||
// Handle imports manually
|
||||
for import_path in extract_import_paths(code)? {
|
||||
let resolved = resolve_import_path(&import_path)?;
|
||||
|
||||
// Prevent infinite recursion
|
||||
let canonical = fs::canonicalize(&resolved).unwrap_or(resolved.clone());
|
||||
if visited.contains(&canonical) {
|
||||
continue;
|
||||
}
|
||||
|
||||
visited.insert(canonical.clone());
|
||||
|
||||
if resolved.exists() {
|
||||
let imported_code = fs::read_to_string(&resolved)?;
|
||||
let inner = extract_poll_and_listen_vars_inner(&imported_code, visited)?;
|
||||
results.extend(inner);
|
||||
}
|
||||
}
|
||||
|
||||
// Process this file’s own poll/listen calls
|
||||
for expr in extract_poll_listen_exprs(code) {
|
||||
match engine.eval_expression::<TempSignal>(&expr) {
|
||||
Ok(sig) => {
|
||||
@@ -23,6 +54,38 @@ pub fn extract_poll_and_listen_vars(code: &str) -> Result<Vec<(String, Option<St
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Extract import paths from the Rhai source code
|
||||
fn extract_import_paths(code: &str) -> Result<Vec<String>> {
|
||||
let mut imports = Vec::new();
|
||||
|
||||
for line in code.lines() {
|
||||
let trimmed = line.trim_start();
|
||||
|
||||
if trimmed.starts_with("import ") {
|
||||
if let Some(start) = trimmed.find('"') {
|
||||
if let Some(end_rel) = trimmed[start + 1..].find('"') {
|
||||
let end = start + 1 + end_rel;
|
||||
let path = &trimmed[start + 1..end];
|
||||
imports.push(path.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(imports)
|
||||
}
|
||||
|
||||
/// Resolve relative and absolute import paths.
|
||||
fn resolve_import_path(import_path: &str) -> Result<PathBuf> {
|
||||
let path = Path::new(import_path);
|
||||
let abs =
|
||||
if path.is_absolute() { path.to_path_buf() } else { std::env::current_dir()?.join(path) };
|
||||
|
||||
let abs = if abs.extension().is_none() { abs.with_extension("rhai") } else { abs };
|
||||
|
||||
Ok(abs)
|
||||
}
|
||||
|
||||
pub fn extract_poll_listen_exprs(code: &str) -> Vec<String> {
|
||||
let mut exprs = Vec::new();
|
||||
let mut i = 0;
|
||||
@@ -35,7 +98,7 @@ pub fn extract_poll_listen_exprs(code: &str) -> Vec<String> {
|
||||
while i < len && code_bytes[i] as char != '\n' {
|
||||
i += 1;
|
||||
}
|
||||
i += 1; // skipp a full line
|
||||
i += 1; // skip a full line
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! IIRhai is a simple crate which configures rhai for the `ewwii` widget system.
|
||||
//! rhai_impl is a simple crate which configures rhai for the `ewwii` widget system.
|
||||
//!
|
||||
//! This crate supports parsing, error handling, and has a custom module_resolver.
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
use crate::error::{format_eval_error, format_parse_error};
|
||||
use crate::parser::ParseConfig;
|
||||
use rhai::{Engine, EvalAltResult, Module, ModuleResolver, Position, AST};
|
||||
use crate::updates::ReactiveVarStore;
|
||||
use rhai::Scope;
|
||||
use rhai::{Dynamic, Engine, EvalAltResult, Module, ModuleResolver, Position, AST};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct SimpleFileResolver;
|
||||
pub struct SimpleFileResolver {
|
||||
pub pl_handler_store: Option<ReactiveVarStore>,
|
||||
}
|
||||
|
||||
impl ModuleResolver for SimpleFileResolver {
|
||||
fn resolve(
|
||||
@@ -50,12 +55,41 @@ impl ModuleResolver for SimpleFileResolver {
|
||||
format_parse_error(&e, &script, full_path.to_str()).into(),
|
||||
))
|
||||
})?;
|
||||
let scope = ParseConfig::initial_poll_listen_scope(&script).map_err(|e| {
|
||||
EvalAltResult::ErrorSystem(
|
||||
format!("error setting up default variables: {full_path:?}"),
|
||||
e.into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let parent_script: Option<String> = if let Some(parent_path) = source_path {
|
||||
match fs::read_to_string(parent_path) {
|
||||
Ok(s) => Some(s),
|
||||
Err(err) => {
|
||||
log::error!("Could not read parent script {parent_path:?}: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut scope = if let Some(ref script) = parent_script {
|
||||
ParseConfig::initial_poll_listen_scope(script).map_err(|e| {
|
||||
EvalAltResult::ErrorSystem(
|
||||
format!("error setting up default variables from {source_path:?}"),
|
||||
e.into(),
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
Scope::new()
|
||||
};
|
||||
|
||||
match &self.pl_handler_store {
|
||||
Some(val) => {
|
||||
let name_to_val: &HashMap<String, String> = &*val.read().unwrap();
|
||||
|
||||
for (name, val) in name_to_val {
|
||||
scope.set_value(name.clone(), Dynamic::from(val.clone()));
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
let mut module = Module::eval_ast_as_new(scope, &ast, engine).map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorSystem(
|
||||
"module_eval_failed".into(),
|
||||
|
||||
@@ -5,37 +5,45 @@ use crate::{
|
||||
helper::extract_poll_and_listen_vars,
|
||||
module_resolver::SimpleFileResolver,
|
||||
providers::register_all_providers,
|
||||
updates::ReactiveVarStore,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use rhai::{Dynamic, Engine, OptimizationLevel, Scope, AST};
|
||||
use rhai::{Dynamic, Engine, ImmutableString, OptimizationLevel, Scope, AST};
|
||||
use std::cell::RefCell;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct ParseConfig {
|
||||
engine: Engine,
|
||||
pub engine: Engine,
|
||||
all_nodes: Rc<RefCell<Vec<WidgetNode>>>,
|
||||
keep_signal: Rc<RefCell<Vec<u64>>>,
|
||||
}
|
||||
|
||||
impl ParseConfig {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(pl_handler_store: Option<ReactiveVarStore>) -> Self {
|
||||
let mut engine = Engine::new();
|
||||
let all_nodes = Rc::new(RefCell::new(Vec::new()));
|
||||
let keep_signal = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
engine.set_max_expr_depths(128, 128);
|
||||
engine.set_module_resolver(SimpleFileResolver);
|
||||
engine
|
||||
.set_module_resolver(SimpleFileResolver { pl_handler_store: pl_handler_store.clone() });
|
||||
|
||||
register_all_widgets(&mut engine, &all_nodes);
|
||||
register_all_providers(&mut engine);
|
||||
register_all_widgets(&mut engine, &all_nodes, &keep_signal);
|
||||
register_all_providers(&mut engine, pl_handler_store);
|
||||
|
||||
Self { engine, all_nodes }
|
||||
Self { engine, all_nodes, keep_signal }
|
||||
}
|
||||
|
||||
pub fn compile_code(&mut self, code: &str, file_path: &str) -> Result<AST> {
|
||||
self.engine
|
||||
let mut ast = self
|
||||
.engine
|
||||
.compile(code)
|
||||
.map_err(|e| anyhow!(format_parse_error(&e, code, Some(file_path))))
|
||||
.map_err(|e| anyhow!(format_parse_error(&e, code, Some(file_path))))?;
|
||||
|
||||
ast.set_source(ImmutableString::from(file_path));
|
||||
Ok(ast)
|
||||
}
|
||||
|
||||
pub fn eval_code_with(
|
||||
@@ -63,6 +71,9 @@ impl ParseConfig {
|
||||
.map_err(|e| anyhow!(format_eval_error(&e, code, &self.engine, file_id)))?;
|
||||
};
|
||||
|
||||
// Retain signals
|
||||
crate::updates::retain_signals(&self.keep_signal.borrow());
|
||||
|
||||
// Merge all nodes in all_nodes (`enter([])`) into a single root node
|
||||
let merged_node = {
|
||||
let mut all_nodes_vec = self.all_nodes.borrow_mut();
|
||||
@@ -84,6 +95,24 @@ impl ParseConfig {
|
||||
Ok(merged_node.setup_dyn_ids("root"))
|
||||
}
|
||||
|
||||
pub fn eval_code_snippet(&mut self, code: &str) -> Result<WidgetNode> {
|
||||
let mut scope = Scope::new();
|
||||
|
||||
// Just eval as node will be in `all_nodes`
|
||||
let node = self
|
||||
.engine
|
||||
.eval_with_scope::<WidgetNode>(&mut scope, code)
|
||||
.map_err(|e| anyhow!(format_eval_error(&e, code, &self.engine, Some("<dyn eval>"))))?;
|
||||
|
||||
// Retain signals
|
||||
crate::updates::retain_signals(&self.keep_signal.borrow());
|
||||
|
||||
// Clear all nodes
|
||||
self.all_nodes.borrow_mut().clear();
|
||||
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
pub fn code_from_file<P: AsRef<Path>>(&mut self, file_path: P) -> Result<String> {
|
||||
Ok(fs::read_to_string(&file_path)
|
||||
.map_err(|e| anyhow!("Failed to read {:?}: {}", file_path.as_ref(), e))?)
|
||||
@@ -146,4 +175,11 @@ impl ParseConfig {
|
||||
pub fn set_opt_level(&mut self, opt_lvl: OptimizationLevel) {
|
||||
self.engine.set_optimization_level(opt_lvl);
|
||||
}
|
||||
|
||||
pub fn action_with_engine<F, R>(&mut self, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut Engine) -> R,
|
||||
{
|
||||
f(&mut self.engine)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,19 +11,23 @@ mod apilib;
|
||||
mod stdlib;
|
||||
|
||||
use crate::module_resolver::{ChainedResolver, SimpleFileResolver};
|
||||
use crate::updates::ReactiveVarStore;
|
||||
use rhai::module_resolvers::StaticModuleResolver;
|
||||
|
||||
// expose the api's publically
|
||||
pub use apilib::register_apilib;
|
||||
pub use stdlib::register_stdlib;
|
||||
|
||||
pub fn register_all_providers(engine: &mut rhai::Engine) {
|
||||
pub fn register_all_providers(engine: &mut rhai::Engine, plhs: Option<ReactiveVarStore>) {
|
||||
let mut resolver = StaticModuleResolver::new();
|
||||
|
||||
// modules
|
||||
register_stdlib(&mut resolver);
|
||||
register_apilib(&mut resolver);
|
||||
|
||||
let chained = ChainedResolver { first: SimpleFileResolver, second: resolver.clone() };
|
||||
let chained = ChainedResolver {
|
||||
first: SimpleFileResolver { pl_handler_store: plhs.clone() },
|
||||
second: resolver.clone(),
|
||||
};
|
||||
engine.set_module_resolver(chained);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@ use rhai::exported_module;
|
||||
use rhai::module_resolvers::StaticModuleResolver;
|
||||
|
||||
pub fn register_stdlib(resolver: &mut StaticModuleResolver) {
|
||||
use crate::providers::stdlib::{
|
||||
command::command, env::env, regex::regex_lib, text::text,
|
||||
};
|
||||
use crate::providers::stdlib::{command::command, env::env, regex::regex_lib, text::text};
|
||||
|
||||
// adding modules
|
||||
let text_mod = exported_module!(text);
|
||||
|
||||
@@ -30,6 +30,7 @@ use tokio::sync::watch;
|
||||
pub fn handle_listen(
|
||||
var_name: String,
|
||||
props: &Map,
|
||||
shell: String,
|
||||
store: ReactiveVarStore,
|
||||
tx: tokio::sync::mpsc::UnboundedSender<String>,
|
||||
) {
|
||||
@@ -76,7 +77,7 @@ pub fn handle_listen(
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut child = unsafe {
|
||||
Command::new("/bin/sh")
|
||||
Command::new(&shell)
|
||||
.arg("-c")
|
||||
.arg(&cmd)
|
||||
// .kill_on_drop(true)
|
||||
|
||||
249
crates/rhai_impl/src/updates/localsignal.rs
Normal file
249
crates/rhai_impl/src/updates/localsignal.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
use super::{get_prefered_shell, handle_listen, handle_poll};
|
||||
use crate::parser::ParseConfig;
|
||||
use gtk4::glib;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::subclass::prelude::*;
|
||||
use once_cell::sync::Lazy;
|
||||
use rhai::Map;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LocalDataBinder {
|
||||
pub value: RefCell<String>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for LocalDataBinder {
|
||||
const NAME: &'static str = "LocalDataBinder";
|
||||
type Type = super::LocalDataBinder;
|
||||
type ParentType = glib::Object;
|
||||
}
|
||||
|
||||
impl ObjectImpl for LocalDataBinder {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: once_cell::sync::Lazy<Vec<glib::ParamSpec>> =
|
||||
once_cell::sync::Lazy::new(|| {
|
||||
vec![glib::ParamSpecString::builder("value")
|
||||
.nick("Value")
|
||||
.blurb("The bound value")
|
||||
.default_value(None)
|
||||
.build()]
|
||||
});
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"value" => self.value.borrow().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"value" => {
|
||||
let val: Option<String> = value.get().unwrap();
|
||||
self.set_value(&self.obj(), val.unwrap_or_default());
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalDataBinder {
|
||||
pub fn set_value(&self, obj: &super::LocalDataBinder, val: String) {
|
||||
*self.value.borrow_mut() = val;
|
||||
obj.notify("value");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct LocalDataBinder(ObjectSubclass<imp::LocalDataBinder>);
|
||||
}
|
||||
|
||||
impl LocalDataBinder {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new::<Self>()
|
||||
}
|
||||
|
||||
pub fn value(&self) -> String {
|
||||
self.imp().value.borrow().clone()
|
||||
}
|
||||
|
||||
pub fn set_value(&self, val: &str) {
|
||||
self.set_property("value", val);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocalSignal {
|
||||
pub id: u64,
|
||||
pub props: Map,
|
||||
pub data: Arc<LocalDataBinder>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
pub static LOCAL_SIGNALS: Lazy<RefCell<HashMap<u64, Rc<LocalSignal>>>> =
|
||||
Lazy::new(|| RefCell::new(HashMap::new()));
|
||||
}
|
||||
|
||||
pub fn register_signal(id: u64, signal: Rc<LocalSignal>) -> Rc<LocalSignal> {
|
||||
LOCAL_SIGNALS.with(|registry| {
|
||||
let mut map = registry.borrow_mut();
|
||||
map.entry(id).or_insert_with(|| signal.clone()).clone()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn retain_signals(ids: &Vec<u64>) {
|
||||
LOCAL_SIGNALS.with(|registry| {
|
||||
let mut map = registry.borrow_mut();
|
||||
map.retain(|id, _| ids.contains(id));
|
||||
});
|
||||
}
|
||||
|
||||
pub fn notify_all_localsignals() {
|
||||
LOCAL_SIGNALS.with(|registry| {
|
||||
let registry_ref = registry.borrow();
|
||||
|
||||
for (_, signal) in registry_ref.iter() {
|
||||
signal.data.notify("value");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn handle_localsignal_changes(
|
||||
parser: Rc<RefCell<ParseConfig>>,
|
||||
ast: Option<Rc<RefCell<rhai::AST>>>,
|
||||
) {
|
||||
let shell = get_prefered_shell();
|
||||
let get_string_fn = shared_utils::extract_props::get_string_prop;
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<String>();
|
||||
let store = Arc::new(RwLock::new(HashMap::new()));
|
||||
|
||||
LOCAL_SIGNALS.with(|registry| {
|
||||
let registry_ref = registry.borrow();
|
||||
|
||||
for (id, signal) in registry_ref.iter() {
|
||||
let props = &signal.props;
|
||||
|
||||
if let Ok(initial_str) = get_string_fn(&props, "initial", None) {
|
||||
signal.data.set_value(&initial_str);
|
||||
}
|
||||
|
||||
match get_string_fn(&props, "type", None) {
|
||||
Ok(signal_type) => match signal_type.to_ascii_lowercase().as_str() {
|
||||
"poll" => handle_poll(
|
||||
id.to_string(),
|
||||
&props,
|
||||
shell.clone(),
|
||||
store.clone(),
|
||||
tx.clone(),
|
||||
),
|
||||
"listen" => handle_listen(
|
||||
id.to_string(),
|
||||
&props,
|
||||
shell.clone(),
|
||||
store.clone(),
|
||||
tx.clone(),
|
||||
),
|
||||
o => log::error!("Invalid type: '{}'", o),
|
||||
},
|
||||
Err(_) => {
|
||||
log::error!(
|
||||
"Unable to handle localsignal {}: 'type' property missing or invalid.",
|
||||
id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
while let Some(id_str) = rx.recv().await {
|
||||
let value_opt = {
|
||||
let guard = store.read().unwrap();
|
||||
guard.get(&id_str).cloned()
|
||||
};
|
||||
|
||||
if let Some(value) = value_opt {
|
||||
if let Ok(id) = id_str.parse::<u64>() {
|
||||
LOCAL_SIGNALS.with(|registry| {
|
||||
let mut registry_ref = registry.borrow_mut();
|
||||
|
||||
if let Some(signal) = registry_ref.get_mut(&id) {
|
||||
let original = value.to_string();
|
||||
let mut current = original.clone();
|
||||
|
||||
let mutations: Vec<rhai::FnPtr> = match signal.props.get("mutations") {
|
||||
Some(v) => {
|
||||
if let Ok(arr) = v.as_array_ref() {
|
||||
arr.iter()
|
||||
.filter_map(|item| {
|
||||
item.clone().try_cast::<rhai::FnPtr>().or_else(|| {
|
||||
log::warn!("Non-function found in signal.props.mutations");
|
||||
None
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
log::warn!("Localsignal mutations property is not an array");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
if mutations.is_empty() {
|
||||
signal.data.set_value(¤t);
|
||||
return;
|
||||
}
|
||||
|
||||
let parser_rc = parser.borrow_mut();
|
||||
let compiled_ast = match ast.as_ref() {
|
||||
Some(rc) => rc.borrow(),
|
||||
None => {
|
||||
log::warn!("No compiled AST available");
|
||||
signal.data.set_value(¤t);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for mutation in mutations {
|
||||
match mutation.call::<String>(&parser_rc.engine, &compiled_ast, (current.clone(),)) {
|
||||
Ok(v) => {
|
||||
current = v;
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Signal {} mutation failed ({}), reverting to original value",
|
||||
id,
|
||||
e
|
||||
);
|
||||
current = original.clone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signal.data.set_value(¤t);
|
||||
} else {
|
||||
log::warn!("No LocalSignal found for id {}", id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log::error!("Invalid id_str '{}': cannot parse to u64", id_str);
|
||||
}
|
||||
} else {
|
||||
log::warn!("No value found in store for id '{}'", id_str);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -15,12 +15,16 @@
|
||||
*/
|
||||
|
||||
mod listen;
|
||||
mod localsignal;
|
||||
mod poll;
|
||||
|
||||
pub use localsignal::*;
|
||||
|
||||
use crate::ast::WidgetNode;
|
||||
use listen::handle_listen;
|
||||
use once_cell::sync::Lazy;
|
||||
use poll::handle_poll;
|
||||
use std::process::Command;
|
||||
use std::sync::Mutex;
|
||||
use std::{collections::HashMap, sync::Arc, sync::RwLock};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
@@ -30,22 +34,30 @@ pub type ReactiveVarStore = Arc<RwLock<HashMap<String, String>>>;
|
||||
pub static SHUTDOWN_REGISTRY: Lazy<Mutex<Vec<watch::Sender<bool>>>> =
|
||||
Lazy::new(|| Mutex::new(Vec::new()));
|
||||
|
||||
pub fn get_prefered_shell() -> String {
|
||||
// Check Dash and prefer if dash is installed.
|
||||
let dash_installed: bool =
|
||||
Command::new("which").arg("dash").output().map(|o| o.status.success()).unwrap_or(false);
|
||||
|
||||
let shell = if dash_installed { String::from("/bin/dash") } else { String::from("/bin/sh") };
|
||||
|
||||
shell
|
||||
}
|
||||
|
||||
pub fn handle_state_changes(
|
||||
root_node: &WidgetNode,
|
||||
tx: UnboundedSender<String>,
|
||||
) -> ReactiveVarStore {
|
||||
// Enter node is the WidgetNode of Enter()
|
||||
// it is the very root of every config.
|
||||
let store: ReactiveVarStore = Arc::new(RwLock::new(HashMap::new()));
|
||||
|
||||
store: ReactiveVarStore,
|
||||
) {
|
||||
let shell = get_prefered_shell();
|
||||
if let WidgetNode::Enter(children) = root_node {
|
||||
for child in children {
|
||||
match child {
|
||||
WidgetNode::Poll { var, props } => {
|
||||
handle_poll(var.to_string(), props, store.clone(), tx.clone());
|
||||
handle_poll(var.to_string(), props, shell.clone(), store.clone(), tx.clone());
|
||||
}
|
||||
WidgetNode::Listen { var, props } => {
|
||||
handle_listen(var.to_string(), props, store.clone(), tx.clone());
|
||||
handle_listen(var.to_string(), props, shell.clone(), store.clone(), tx.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -53,8 +65,6 @@ pub fn handle_state_changes(
|
||||
} else {
|
||||
log::warn!("Expected Enter() as root node for config");
|
||||
}
|
||||
|
||||
store
|
||||
}
|
||||
|
||||
pub fn kill_state_change_handler() {
|
||||
|
||||
@@ -26,6 +26,7 @@ use tokio::time::sleep;
|
||||
pub fn handle_poll(
|
||||
var_name: String,
|
||||
props: &Map,
|
||||
shell: String,
|
||||
store: ReactiveVarStore,
|
||||
tx: tokio::sync::mpsc::UnboundedSender<String>,
|
||||
) {
|
||||
@@ -41,6 +42,14 @@ pub fn handle_poll(
|
||||
}
|
||||
};
|
||||
|
||||
// Skip unchanged values?
|
||||
const DEFAULT_SKIP: bool = true;
|
||||
let skip_unchanged =
|
||||
get_bool_prop(props, "skip_unchanged", Some(DEFAULT_SKIP)).unwrap_or_else(|e| {
|
||||
log::warn!("Failed to parse skip_unchanged property of poll {}: {}", var_name, e);
|
||||
DEFAULT_SKIP
|
||||
});
|
||||
|
||||
// No need to do this as we apply the initial value before parsing
|
||||
// Handle initial value
|
||||
// if let Ok(initial) = get_string_prop(&props, "initial", None) {
|
||||
@@ -57,7 +66,7 @@ pub fn handle_poll(
|
||||
|
||||
tokio::spawn(async move {
|
||||
// Spawn a persistent shell
|
||||
let mut child = match Command::new("/bin/sh")
|
||||
let mut child = match Command::new(&shell)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
@@ -95,12 +104,18 @@ pub fn handle_poll(
|
||||
if let Ok(Some(stdout_line)) = output_line {
|
||||
let stdout_trimmed = stdout_line.trim().to_string();
|
||||
if Some(&stdout_trimmed) != last_value.as_ref() {
|
||||
// changed → always send
|
||||
last_value = Some(stdout_trimmed.clone());
|
||||
log::debug!("[{}] polled value: {}", var_name, stdout_trimmed);
|
||||
store.write().unwrap().insert(var_name.clone(), stdout_trimmed);
|
||||
let _ = tx.send(var_name.clone());
|
||||
} else {
|
||||
} else if skip_unchanged {
|
||||
// unchanged + skipping enabled
|
||||
log::trace!("[{}] value unchanged, skipping tx", var_name);
|
||||
} else {
|
||||
// unchanged + skipping disabled → still send
|
||||
log::trace!("[{}] value unchanged, skipping disabled", var_name);
|
||||
let _ = tx.send(var_name.clone());
|
||||
}
|
||||
} else {
|
||||
log::warn!("[{}] shell output ended or failed: {:?}", var_name, output_line);
|
||||
|
||||
@@ -4,11 +4,12 @@ version = "0.1.0"
|
||||
authors = ["byson94 <byson94wastaken@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
description = "Utility crate used in eww"
|
||||
repository = "https://github.com/elkowar/eww"
|
||||
homepage = "https://github.com/elkowar/eww"
|
||||
description = "Utility crate used in ewwii"
|
||||
repository = "https://github.com/ewwii-sh/ewwii"
|
||||
homepage = "https://github.com/ewwii-sh/ewwii"
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
rhai.workspace = true
|
||||
anyhow.workspace = true
|
||||
rhai = { workspace = true, features = ["internals"] }
|
||||
anyhow.workspace = true
|
||||
once_cell.workspace = true
|
||||
20
examples/activateLinux/ewwii.rhai
Normal file
20
examples/activateLinux/ewwii.rhai
Normal file
@@ -0,0 +1,20 @@
|
||||
enter([
|
||||
defwindow(
|
||||
"activate linux",
|
||||
#{
|
||||
monitor: 0,
|
||||
focusable: "none",
|
||||
stacking: "overlay",
|
||||
wm_ignore: false,
|
||||
geometry: #{
|
||||
x: "50px",
|
||||
y: "20px",
|
||||
width: "50px",
|
||||
height: "30px",
|
||||
anchor: "bottom right",
|
||||
},
|
||||
reserve: #{ distance: "40px", side: "top" },
|
||||
},
|
||||
label(#{ markup: "<big>Activate linux</big>\nGo to Settings to activate Linux", justify: "left",class: "activate"})
|
||||
),
|
||||
]);
|
||||
9
examples/activateLinux/ewwii.scss
Normal file
9
examples/activateLinux/ewwii.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
* {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
|
||||
.activate {
|
||||
background: transparent;
|
||||
color: gray;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fn bar(music_var, volume, time) {
|
||||
return centerbox(#{ orientation: "h" }, [
|
||||
return box(#{ orientation: "h" }, [
|
||||
workspaces(),
|
||||
music(music_var),
|
||||
sidestuff(volume, time),
|
||||
@@ -66,7 +66,7 @@ fn metric(props) {
|
||||
space_evenly: false,
|
||||
}, [
|
||||
box(#{ class: "label" }, [ label(#{ text: label_prop }) ]),
|
||||
slider(#{
|
||||
scale(#{
|
||||
min: 0,
|
||||
max: 101,
|
||||
active: onchange_prop != "",
|
||||
|
||||
17
proc_macros/scan_prop_proc/Cargo.toml
Normal file
17
proc_macros/scan_prop_proc/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "scan_prop_proc"
|
||||
version = "0.1.0"
|
||||
authors = ["byson94 <byson94wastaken@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
description = "A procedual macro for generating properties on a WidgetNode"
|
||||
repository = "https://github.com/byson94/ewwii"
|
||||
homepage = "https://github.com/byson94/ewwii"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn.workspace = true
|
||||
quote.workspace = true
|
||||
proc-macro2.workspace = true
|
||||
47
proc_macros/scan_prop_proc/src/lib.rs
Normal file
47
proc_macros/scan_prop_proc/src/lib.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, Data, DeriveInput, Fields};
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn scan_prop(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(item as DeriveInput);
|
||||
let name = &input.ident;
|
||||
|
||||
let props_matches = if let Data::Enum(data_enum) = &input.data {
|
||||
data_enum
|
||||
.variants
|
||||
.iter()
|
||||
.filter_map(|v| match &v.fields {
|
||||
Fields::Named(fields) => {
|
||||
for f in &fields.named {
|
||||
if f.ident.as_ref().map(|id| id == "props").unwrap_or(false) {
|
||||
let vname = &v.ident;
|
||||
return Some(quote! {
|
||||
#name::#vname { props, .. } => Some(props)
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let expanded = quote! {
|
||||
#input
|
||||
|
||||
impl #name {
|
||||
pub fn props(&self) -> Option<&Map> {
|
||||
match self {
|
||||
#(#props_matches),*,
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
@@ -5,5 +5,5 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
rhai_impl.workspace = true
|
||||
rhai = "1.22.2"
|
||||
rhai.workspace = true
|
||||
rhai-autodocs = "0.9.0"
|
||||
|
||||
Reference in New Issue
Block a user