Compare commits
1425 Commits
Author | SHA1 | Date | |
---|---|---|---|
2dc42b4a22 | |||
343e6332d7 | |||
3a5f866782 | |||
e4b626b91f | |||
45ecf02504 | |||
|
1b6cf05f5f | ||
|
c6c3594c83 | ||
|
06d0158efb | ||
|
612585109f | ||
|
96a462d2f2 | ||
|
9ae22b58d8 | ||
|
d2c8a3e04b | ||
|
ba51661a2a | ||
|
80df17ab55 | ||
|
b62fcca9b9 | ||
|
94ee2566c4 | ||
|
bb6e06127e | ||
|
15c17a7250 | ||
|
a53e7559fe | ||
|
c70f90f320 | ||
|
27b42ca6b6 | ||
|
83d19689c8 | ||
|
e68da229b3 | ||
|
d1797dda87 | ||
|
d46dd67a91 | ||
|
e66120b9c1 | ||
|
dbc1181011 | ||
|
f6ff87bb8f | ||
|
56f673d819 | ||
|
5224cc7f26 | ||
|
656dc357f8 | ||
|
0e70b11e00 | ||
|
ce3a5f19b9 | ||
|
3051d4ec76 | ||
|
57510c43c8 | ||
|
827577c179 | ||
|
f7d6e40bf0 | ||
|
397a5c06ec | ||
|
d3d6f3efb3 | ||
|
ef5f46bcdd | ||
|
a32ce271bd | ||
|
2a5aa69024 | ||
|
e00fca6372 | ||
|
c0cfd9d5cf | ||
|
be3ee9adf1 | ||
|
1ef38cc207 | ||
|
7a626f3b7b | ||
|
aa18a6e0d5 | ||
|
bab71971fb | ||
|
88da268724 | ||
|
7370dfac6e | ||
|
52a185fbab | ||
|
1dce929dfc | ||
|
aab2176802 | ||
|
8a3b71df8b | ||
|
b94cd7a2a8 | ||
|
2e17d7860b | ||
|
0a14492343 | ||
|
92361ef07d | ||
|
d83e263abe | ||
|
fe0a27c56d | ||
|
903570846e | ||
|
a9eb591628 | ||
|
662889bb83 | ||
|
f04bc94b80 | ||
|
e292821c37 | ||
|
d4c5f3ee95 | ||
|
1a2fd4e743 | ||
|
e6f8b32583 | ||
|
df971ac99b | ||
|
4cbc345245 | ||
|
ebec84ea7c | ||
|
0772310c4f | ||
|
a70715ad9e | ||
|
2d549d806c | ||
|
b40c441646 | ||
|
d91422e345 | ||
|
ae484429d3 | ||
|
cf6af4c256 | ||
|
8b7452a55d | ||
|
3918257883 | ||
|
59f3896392 | ||
|
40d21d2365 | ||
|
0ce6a2db21 | ||
|
70ee917bac | ||
|
88defb65ca | ||
|
faa68534cf | ||
|
5f1b880521 | ||
|
e6db439870 | ||
|
77a17cde83 | ||
|
08f5591148 | ||
|
3840b75bea | ||
|
6b4bf34bf9 | ||
|
59c2500c55 | ||
|
983aa1b366 | ||
|
0722d5e3ec | ||
|
c719091c3d | ||
|
9a2c78c405 | ||
|
4b8ba8309f | ||
|
84c0c9bc7a | ||
|
6f6f1d82e8 | ||
|
cdab8f90fb | ||
|
3ea2c4595a | ||
|
8473d8e984 | ||
|
854b2e0c4d | ||
|
ca4257ff5c | ||
|
90fee3a923 | ||
|
06517aa7e8 | ||
|
0864e63bde | ||
|
2b20512a3d | ||
|
b9ec44446e | ||
|
8f952452ce | ||
|
67db13ff7c | ||
|
00de9d0c9b | ||
|
bbb4e4678f | ||
|
1207afa7d0 | ||
|
bf09f44d56 | ||
|
883c7e71ae | ||
|
ab8dcf91bd | ||
|
2b520ca098 | ||
|
d66a34b272 | ||
|
0333a8daff | ||
|
c1f70f1e92 | ||
|
7da3fb1b22 | ||
|
a365998264 | ||
|
ae517b9fa0 | ||
|
7eb5127748 | ||
|
939fcfe9db | ||
|
17dcfa8faf | ||
|
21971e0037 | ||
|
35425001ed | ||
|
b22b0dd7ff | ||
|
bbca5c4bde | ||
|
a74974a8e4 | ||
|
5f27ed4720 | ||
|
c51b771519 | ||
|
f52f8c1337 | ||
|
a5fddf9ee6 | ||
|
2736e4ca35 | ||
|
5e2743361b | ||
|
4e923290cc | ||
|
9d4996cbab | ||
|
2f766b7341 | ||
|
9f60a256fc | ||
|
81c75c1524 | ||
|
6759a5c56f | ||
|
842d54732b | ||
|
ec20763aef | ||
|
9ba9558429 | ||
|
a19f89d3a6 | ||
|
4de164dcc7 | ||
|
e1b1227f0c | ||
|
8f126c212b | ||
|
9f5c2eb0c4 | ||
|
9e453843b2 | ||
|
2f10a1f2a2 | ||
|
74b9ea520f | ||
|
7eefad34fc | ||
|
15c4901aba | ||
|
ffdf87fbe2 | ||
|
5fc4210270 | ||
|
b0d1be69d6 | ||
|
09a5348740 | ||
|
1fb60c5dcb | ||
|
bd6b8304bd | ||
|
c5d6ddc126 | ||
|
4a6db30d47 | ||
|
e2595ac0aa | ||
|
5f935c34fd | ||
|
98d108d2b7 | ||
|
2d76c7165c | ||
|
4a727dfb8b | ||
|
12c0c18d66 | ||
|
c7898a101c | ||
|
2969558afd | ||
|
14d900d835 | ||
|
8d882d787e | ||
|
d1c4239ac7 | ||
|
9da0a3de54 | ||
|
803cc88483 | ||
|
2056304e39 | ||
|
fd2c96c8e3 | ||
|
bcc54b0831 | ||
|
a42a0844c2 | ||
|
984583b0cc | ||
|
45dc02e9bd | ||
|
0b140488b4 | ||
|
0646280d67 | ||
|
049999b982 | ||
|
766bb7f5cc | ||
|
c542224f4b | ||
|
b2415eee61 | ||
|
92d647d1a6 | ||
|
c7f6b2a5c7 | ||
|
e7344d03b4 | ||
|
31522e30aa | ||
|
80a490ebb9 | ||
|
09ea73bd2a | ||
|
50452e62b4 | ||
|
2665860b4d | ||
|
a7bb7bb2ea | ||
|
f8aef03456 | ||
|
e815c5bbd7 | ||
|
f2c2f3fc75 | ||
|
a366600e73 | ||
|
4bbfdef14b | ||
|
81f37123ad | ||
|
86361f003c | ||
|
0e9f649cb7 | ||
|
10d6df73e3 | ||
|
c54a6446a3 | ||
|
6a51282933 | ||
|
149098cb68 | ||
|
43ef85ae5c | ||
|
8245a11766 | ||
|
4eb5779542 | ||
|
cf97982929 | ||
|
64af860ad2 | ||
|
e6f23e3771 | ||
|
ca3e4e9f1b | ||
|
add167d6a0 | ||
|
21b10dc103 | ||
|
0cdf8d56ee | ||
|
71de341684 | ||
|
74ee7cca81 | ||
|
0b73e5fbfa | ||
|
8fb0ede72e | ||
|
07b570036a | ||
|
89e604d160 | ||
|
277ae74d68 | ||
|
70164f68a6 | ||
|
056f7e6951 | ||
|
3bdf931925 | ||
|
09110a93b0 | ||
|
a42b3c6998 | ||
|
742086e190 | ||
|
fb478a4014 | ||
|
f54590d7ad | ||
|
d16b9cf7cd | ||
|
f78108a514 | ||
|
745aa49025 | ||
|
b5c41a8815 | ||
|
35df13686f | ||
|
29ad61d377 | ||
|
7de72d6a27 | ||
|
278d40d8c6 | ||
|
11798dcc03 | ||
|
0525d76ff9 | ||
|
d8c3982160 | ||
|
a9ea921383 | ||
|
ebe34b03cb | ||
|
50a1f78996 | ||
|
c23136a5df | ||
|
955b62ea2d | ||
|
9991a89428 | ||
|
1f7e8b7f3d | ||
|
01322f69a4 | ||
|
d612bf5678 | ||
|
08e0b9ffbd | ||
|
bffaeed9fd | ||
|
775c1c5873 | ||
|
18aa14c7bb | ||
|
87c9df294c | ||
|
1d23db1866 | ||
|
d275a4ed32 | ||
|
782dd2f522 | ||
|
8f081bad77 | ||
|
556cf24b95 | ||
|
5615643c52 | ||
|
1916755b6b | ||
|
f41eacc3dc | ||
|
0f00d14297 | ||
|
77c7ad1fef | ||
|
ea1a7248d2 | ||
|
f259d44186 | ||
|
3296be845c | ||
|
209056e1cd | ||
|
1af6fb3ec0 | ||
|
140bea352e | ||
|
c03d46719e | ||
|
df712f9ccf | ||
|
62295f554b | ||
|
bf2d2561b8 | ||
|
69c50c8511 | ||
|
33d80b5a0b | ||
|
6e9bd0d9d1 | ||
|
3aca177132 | ||
|
bf6c65b5ad | ||
|
c361fe48c7 | ||
|
fd275a2fee | ||
|
451bf8dc84 | ||
|
18172f80c9 | ||
|
7ff95f3a88 | ||
|
31c509b206 | ||
|
3bcee62beb | ||
|
04468a7147 | ||
|
a44cd07212 | ||
|
3f968b8c87 | ||
|
1db11ba69a | ||
|
73811c394a | ||
|
0400f6716b | ||
|
2f5a3dacd9 | ||
|
86fa12229e | ||
|
1475b10dff | ||
|
6f52eb4d3a | ||
|
fa98dbad16 | ||
|
45c749f28c | ||
|
9d4f664c94 | ||
|
b86accfe1c | ||
|
fb3cd68dde | ||
|
0c2787eada | ||
|
17a4647fce | ||
|
860de97b42 | ||
|
3b2ed0d6f0 | ||
|
d6d5cf0294 | ||
|
c53022e8df | ||
|
8008ea5286 | ||
|
5681c83d3c | ||
|
d0402d072d | ||
|
1a2cb2f35b | ||
|
f7d67598cb | ||
|
da1a3eed1e | ||
|
ef76f16900 | ||
|
66509784cb | ||
|
a4ad1b297e | ||
|
631c9e4a21 | ||
|
e966cd5b59 | ||
|
eacb2e913f | ||
|
d5a15419e9 | ||
|
6619fab044 | ||
|
88993fb092 | ||
|
86b26f65d6 | ||
|
fee46fd653 | ||
|
ed2b9a91b4 | ||
|
ee38b04d8b | ||
|
c153d11aa6 | ||
|
e6131783e9 | ||
|
1d51025993 | ||
|
28280600d7 | ||
|
2b1d438ad4 | ||
|
4dc93e8138 | ||
|
f46431600c | ||
|
820abdc98e | ||
|
16646ffc41 | ||
|
b8825966d4 | ||
|
dd1b1cac0a | ||
|
b40775fb74 | ||
|
9090fa6a22 | ||
|
91b1886968 | ||
|
7f02765214 | ||
|
d328b07b39 | ||
|
8a3ce90959 | ||
|
2ca05520ba | ||
|
ed458d33d9 | ||
|
7f66345d5a | ||
|
d3b04bf892 | ||
|
e43a46952a | ||
|
f05578c8f8 | ||
|
09e67c5c27 | ||
|
23647d2f50 | ||
|
befeb32225 | ||
|
5d0f7ba3fc | ||
|
461499dd62 | ||
|
ae5a2502d6 | ||
|
d7a5e54455 | ||
|
17f0db57c3 | ||
|
4910e03833 | ||
|
be61d84cae | ||
|
e6bcb7211f | ||
|
2e0ba65a20 | ||
|
16cd38a198 | ||
|
159e8a6abc | ||
|
7c24277210 | ||
|
8346209c37 | ||
|
c393e450a1 | ||
|
f2247a70dc | ||
|
7dc762ad9c | ||
|
4f2962d73f | ||
|
49076c6ac2 | ||
|
c1f79b40cf | ||
|
faa12382d4 | ||
|
f6dda3b2f5 | ||
|
ff15ebc547 | ||
|
44118263b5 | ||
|
07880c392c | ||
|
7559e4fb30 | ||
|
8d605be4e3 | ||
|
3eb63762c7 | ||
|
b689778ed9 | ||
|
bae0a3e46e | ||
|
bb5f034e08 | ||
|
ecbee66bd6 | ||
|
83e037829c | ||
|
67d90e3946 | ||
|
fb015a85d2 | ||
|
56273c5a3c | ||
|
f025794985 | ||
|
1b980bc6e8 | ||
|
72f89ba77f | ||
|
2ce5df0844 | ||
|
fed30ef775 | ||
|
32561bd85c | ||
|
8d68c8584e | ||
|
bbc6e91429 | ||
|
2a46ed8753 | ||
|
6de9a5e076 | ||
|
fe75646f82 | ||
|
24297b11ee | ||
|
02ca90a22d | ||
|
a490fd54c9 | ||
|
b90e5c4e05 | ||
|
00d66da0ce | ||
|
9ba4cfd23f | ||
|
72cfe85f7f | ||
|
ac74f35424 | ||
|
ab0da5dad0 | ||
|
174565715e | ||
|
869fa6baa8 | ||
|
4ab9992bf8 | ||
|
51c22f2426 | ||
|
5fd239c488 | ||
|
795da598e0 | ||
|
7977e970ca | ||
|
55d2c5886f | ||
|
a165eddd75 | ||
|
a225a218e0 | ||
|
aaf5986123 | ||
|
bf5eaca2f2 | ||
|
d4ef470b4f | ||
|
3d91926a74 | ||
|
35b1b8b0e7 | ||
|
021b173ef3 | ||
|
9b778006ce | ||
|
4030326a35 | ||
|
da5da3958e | ||
|
31c30fedd5 | ||
|
73d1353976 | ||
|
62ec03a0af | ||
|
4314ce36f4 | ||
|
a1210c9dae | ||
|
855c0faa59 | ||
|
26fbc9af00 | ||
|
94383d82a5 | ||
|
b64e0ea5e3 | ||
|
73b8ae8e5e | ||
|
2118a726f8 | ||
|
dc0e423142 | ||
|
f24e03eae8 | ||
|
1070b61f34 | ||
|
ce8cb228ef | ||
|
46ce3a1f00 | ||
|
3c5921f30c | ||
|
6820eea9ce | ||
|
67f4c9aea3 | ||
|
1319c25f20 | ||
|
ef553cd124 | ||
|
2f02ca3248 | ||
|
ad19c1628d | ||
|
2334c7d1d5 | ||
|
e3cd947437 | ||
|
733ec6b2ea | ||
|
f3dfaa2c43 | ||
|
33e6682882 | ||
|
21b583c468 | ||
|
105c0fe478 | ||
|
aa0ec2821e | ||
|
69ac47f463 | ||
|
1c12bad866 | ||
|
7a105ade27 | ||
|
9fa0b4da5d | ||
|
625979b665 | ||
|
61f22b1db2 | ||
|
0ff5a02550 | ||
|
a264236624 | ||
|
f655d9b967 | ||
|
e29feef8ba | ||
|
ada8d7c77f | ||
|
f7a370b6b9 | ||
|
afd9274de2 | ||
|
b1afadf1a2 | ||
|
f064f0482b | ||
|
c901f40fd6 | ||
|
5c4f5ae5ec | ||
|
dfeb3db003 | ||
|
946bbd2683 | ||
|
3f44d331d9 | ||
|
99a50d6b2f | ||
|
79aa225001 | ||
|
5190bcea80 | ||
|
75464ad894 | ||
|
d873c37e31 | ||
|
08e13e00f1 | ||
|
ffd195cdb5 | ||
|
a0cc7e4e43 | ||
|
9a037a23e9 | ||
|
e8aeb86698 | ||
|
78cb805fac | ||
|
e50c61f7ff | ||
|
cee8400663 | ||
|
23f753e599 | ||
|
cb530ccf2f | ||
|
1bc69e7a8a | ||
|
b5d842f877 | ||
|
754b8f819e | ||
|
038f75eef1 | ||
|
65a4dca0d9 | ||
|
ab53df8e9d | ||
|
f30a666dc8 | ||
|
bbdf558bd7 | ||
|
eec65a055f | ||
|
1432c82bdf | ||
|
c9696ca687 | ||
|
b3c192a2e4 | ||
|
d19c02035f | ||
|
50c37ff3d7 | ||
|
f5e16312bf | ||
|
6b4a4655c1 | ||
|
d7d2e0a8aa | ||
|
3cc32abc7c | ||
|
7bc7b60321 | ||
|
4aed3ede92 | ||
|
c71d83fe0e | ||
|
0b819de3e2 | ||
|
f131969c47 | ||
|
4c0286e8ac | ||
|
2a516dfc48 | ||
|
7b6f1baa69 | ||
|
c3643eaf6d | ||
|
072ec69d53 | ||
|
3cfe6e428b | ||
|
baa1389f71 | ||
|
49dbf2c146 | ||
|
c0a5dc980e | ||
|
8b93c9cb6a | ||
|
5260b3072a | ||
|
ac7816e8ca | ||
|
172bd78a91 | ||
|
8493ccec7f | ||
|
dcc4bb77e9 | ||
|
be0cc2c780 | ||
|
041cab0fa4 | ||
|
4960a8827e | ||
|
40750d9b36 | ||
|
9e01adc964 | ||
|
19c07da86f | ||
|
98cf9c455a | ||
|
0d119aa731 | ||
|
6a2c73d0e0 | ||
|
ca6ff874a1 | ||
|
fd3801ed38 | ||
|
4dc5bffdfb | ||
|
e8e656b330 | ||
|
4e1e0e0890 | ||
|
a11bcf5af0 | ||
|
969de1d31c | ||
|
4c494c7244 | ||
|
d5b9dee2fd | ||
|
e7f0d3809b | ||
|
99eda093d6 | ||
|
94af348846 | ||
|
ede4440e99 | ||
|
b96d87ff69 | ||
|
2ba9598f80 | ||
|
d34f8e06c8 | ||
|
a65d6a11cb | ||
|
86f0e3d4e2 | ||
|
ae9521e500 | ||
|
05750bf186 | ||
|
6cb90570a3 | ||
|
6d73b94e9a | ||
|
96b2afba31 | ||
|
f8cb3acc48 | ||
|
3ef49cd51c | ||
|
0691ec3195 | ||
|
490a437b2f | ||
|
ab0ee1a8d0 | ||
|
01a4d65404 | ||
|
8a864fcce9 | ||
|
0cde20b355 | ||
|
67b6f044e8 | ||
|
e11b5c614f | ||
|
6de0a2c371 | ||
|
7f9813d4b3 | ||
|
d3db055583 | ||
|
709ed1f3f7 | ||
|
0a775191ab | ||
|
16c1261d82 | ||
|
ead4186870 | ||
|
40501f630d | ||
|
4aa0d7a13a | ||
|
ef28347f1c | ||
|
508128436c | ||
|
b9d42a45a8 | ||
|
2ca7e3c4b0 | ||
|
c7bb434113 | ||
|
45511a442f | ||
|
5dd62bacd5 | ||
|
823ea15732 | ||
|
de176beb28 | ||
|
858eafe22e | ||
|
22ced3485e | ||
|
5d045c2e9a | ||
|
431171f975 | ||
|
6bf459e068 | ||
|
7f7e803448 | ||
|
0d8cefbf2d | ||
|
fbe3aa3cf4 | ||
|
9079014974 | ||
|
334dd09817 | ||
|
f5ee6d0e51 | ||
|
5324eb1024 | ||
|
230bd6f747 | ||
|
2ab7341fa5 | ||
|
32b9c1fdbd | ||
|
c620e4dc4c | ||
|
f3d54a0f33 | ||
|
d1f2a18439 | ||
|
d6bf8955db | ||
|
1dd79c4697 | ||
|
6f71a8e3d5 | ||
|
1b287cddaf | ||
|
c5545c7a73 | ||
|
bbfb1c040c | ||
|
2798d4935e | ||
|
60dcfc354e | ||
|
d54f17c6aa | ||
|
ae5e2c6c73 | ||
|
d77492c0c3 | ||
|
e0e4ee73fe | ||
|
a1a5fcfd46 | ||
|
720e7756f2 | ||
|
e618091248 | ||
|
c2e0c52ce0 | ||
|
f0480854a9 | ||
|
e0c4f1a08e | ||
|
4aed0fa4b6 | ||
|
a0ac09122a | ||
|
750a441a8c | ||
|
05af8d00d4 | ||
|
d4743183d4 | ||
|
33448508a5 | ||
|
175f5d71f4 | ||
|
863714602c | ||
|
e89e521e2b | ||
|
cd6064fd84 | ||
|
7565d294af | ||
|
4406494ff3 | ||
|
26d93004a6 | ||
|
3a5a07d49d | ||
|
7dc02a5e16 | ||
|
93c6be5eef | ||
|
80e2d1b08b | ||
|
67b2ccb4d5 | ||
|
27aad74a32 | ||
|
c0fd5de8a0 | ||
|
e3555174d7 | ||
|
3d3e51a742 | ||
|
1a8d253611 | ||
|
24574b355d | ||
|
2f41ccee1c | ||
|
917199197f | ||
|
4417a34edb | ||
|
5aaaea7c88 | ||
|
c3c5161386 | ||
|
a43fb42428 | ||
|
cc8f5b6fc8 | ||
|
0025b8c3f8 | ||
|
916a1bfc70 | ||
|
8fa9e4c94e | ||
|
4fd8e47737 | ||
|
03ca7eea6c | ||
|
573929d5ec | ||
|
555371f77e | ||
|
4d724a88e6 | ||
|
0a011f9031 | ||
|
11e4039b56 | ||
|
e7e8e76c28 | ||
|
1833c77312 | ||
|
ffbe59f812 | ||
|
0a5f1bb676 | ||
|
c23995ecb4 | ||
|
f9227546ca | ||
|
404122e0b1 | ||
|
08b376c6d7 | ||
|
345f0e1336 | ||
|
980ac6c2a4 | ||
|
377ead93d6 | ||
|
a6db1e1fb3 | ||
|
71323c51bb | ||
|
ee97887409 | ||
|
005ad6215a | ||
|
98bc8cf2a7 | ||
|
d4c4198f72 | ||
|
1501a93915 | ||
|
e2076612cb | ||
|
bb9ccc4f62 | ||
|
137664ca88 | ||
|
d8b9e03481 | ||
|
e55cd9652e | ||
|
e139aae143 | ||
|
af95d3972e | ||
|
611d9e399c | ||
|
5e014a70e8 | ||
|
38c4dd5fdb | ||
|
5d12e194f4 | ||
|
70f86f998b | ||
|
afa0bca4fd | ||
|
61c707fe04 | ||
|
dc51080328 | ||
|
5586034d66 | ||
|
475a2779a7 | ||
|
ec712c8032 | ||
|
52719c7076 | ||
|
0509710cc5 | ||
|
59403b6ca8 | ||
|
85dc07c3b0 | ||
|
2539042b71 | ||
|
7f1e7aea07 | ||
|
e65585ae17 | ||
|
69c60d372c | ||
|
5d5e60a5cc | ||
|
fd1ceac363 | ||
|
e4eb0553de | ||
|
2ca73036ab | ||
|
59b1e90661 | ||
|
2381a9310c | ||
|
b0825ce38b | ||
|
56dbd68326 | ||
|
20d79a43cc | ||
|
6c2e28d20e | ||
|
dc97d6f33e | ||
|
430f78a693 | ||
|
e0aa89cee7 | ||
|
da2ab420ce | ||
|
3f4770fd28 | ||
|
e87f3acff4 | ||
|
f9bfa82ebc | ||
|
63f54edf0c | ||
|
f1e18d0935 | ||
|
ce2ed35a1a | ||
|
3e71eaee37 | ||
|
758a444d7f | ||
|
27fadad324 | ||
|
4b90241ea1 | ||
|
39fd8ad9e9 | ||
|
b1328f193c | ||
|
6b18e78e53 | ||
|
664a63a4b8 | ||
|
71657b50dd | ||
|
04be010fbd | ||
|
ea3b7b5282 | ||
|
56ce01e262 | ||
|
a95d494f70 | ||
|
408e9e566f | ||
|
7b15e4b0e2 | ||
|
e926e43742 | ||
|
9a4ad3d6a7 | ||
|
fd484c7638 | ||
|
bb44398819 | ||
|
4009f0cf73 | ||
|
831a07f720 | ||
|
63933e26d2 | ||
|
0ff3cbf543 | ||
|
27a4cbccea | ||
|
62fddce2e6 | ||
|
70081c9649 | ||
|
0300b649d7 | ||
|
6786b8a3aa | ||
|
3375824630 | ||
|
f30043ddc2 | ||
|
852d59752e | ||
|
f59832e88e | ||
|
592cc68506 | ||
|
bb424e54c5 | ||
|
a97acd8fa8 | ||
|
edd01159e0 | ||
|
8ade09a0f6 | ||
|
0b5028b1ab | ||
|
462ba3b2c8 | ||
|
90c3a183d5 | ||
|
ae546a5b9d | ||
|
b44f14e186 | ||
|
b23945c78a | ||
|
99e020c7b5 | ||
|
4808fcbd7e | ||
|
f37a658962 | ||
|
5cb40dc095 | ||
|
99352f02fd | ||
|
d0ebcdb936 | ||
|
88b7dd6601 | ||
|
f7825fd936 | ||
|
6d7f2b30cc | ||
|
67fd107746 | ||
|
3df49bebd4 | ||
|
46cd0891d2 | ||
|
c545af3577 | ||
|
5c923fce48 | ||
|
a2296b466b | ||
|
6779fcf621 | ||
|
dce1034699 | ||
|
81096ef454 | ||
|
2a795faf4e | ||
|
2fce83b205 | ||
|
ce65097834 | ||
|
5e47238489 | ||
|
bc70ba12f1 | ||
|
41f6a325e9 | ||
|
f7d7ab1ba9 | ||
|
f652e84187 | ||
|
4b0cc178dd | ||
|
e941eab4ab | ||
|
47d44af348 | ||
|
b549b509c9 | ||
|
19f6a5e2fd | ||
|
867dad62fa | ||
|
d3dee849b7 | ||
|
9afa31899f | ||
|
10f5f95a80 | ||
|
d51b501d2f | ||
|
6e7769b65d | ||
|
d65d838869 | ||
|
d807ef367e | ||
|
703beae05e | ||
|
6e386312bd | ||
|
0f60253661 | ||
|
3c47e3e229 | ||
|
6ca97788b2 | ||
|
0766da7509 | ||
|
ad0a6c4c10 | ||
|
1a9bfd9e73 | ||
|
15f5b93a0d | ||
|
335ad1dd1d | ||
|
7479ba137f | ||
|
070e8e70e4 | ||
|
5198f8e3e4 | ||
|
fd064ff990 | ||
|
d0cca658a1 | ||
|
d7a3ce4e68 | ||
|
2682079713 | ||
|
3aafd2c1f7 | ||
|
4a498ed0e3 | ||
|
743c8851d4 | ||
|
0f745f0c14 | ||
|
2ef1b4317a | ||
|
8c8325d0cf | ||
|
703f7657e1 | ||
|
c114be034a | ||
|
15aa40df40 | ||
|
7d40b76b02 | ||
|
90cf272d67 | ||
|
e1e42f87bd | ||
|
48b90a7529 | ||
|
1b0cb0d13a | ||
|
1f85e1167c | ||
|
512dbd5076 | ||
|
4fc17dfd66 | ||
|
c2cb1a0c81 | ||
|
f5e7e0625e | ||
|
4c44517556 | ||
|
f4f8f62f55 | ||
|
ac8875f81d | ||
|
26cd6c0f2b | ||
|
26a35b7d7e | ||
|
c73f23c380 | ||
|
4979635764 | ||
|
2fa6edf7a8 | ||
|
92e148822f | ||
|
08da96c1c6 | ||
|
3bf938f65f | ||
|
e79e832092 | ||
|
327347501c | ||
|
990b4aa0b7 | ||
|
291dc728a4 | ||
|
4ca54836a8 | ||
|
749a9588d7 | ||
|
f4776a46bb | ||
|
ae009158cc | ||
|
f0ebcc2474 | ||
|
148a4dd469 | ||
|
c4c5216e3b | ||
|
6e9ab1cd6f | ||
|
d07304fb85 | ||
|
0539cd1b85 | ||
|
5b2c71a708 | ||
|
01bc95c2a3 | ||
|
3f6c6b7b02 | ||
|
9e139e6ca7 | ||
|
79fed3d695 | ||
|
8b49968cab | ||
|
49bcebb1e5 | ||
|
3f7dd50594 | ||
|
f9b73607f5 | ||
|
442a52be07 | ||
|
cbe9298a0b | ||
|
57b2daa57e | ||
|
ebc9d275a4 | ||
|
088ba527df | ||
|
e23e93218c | ||
|
4d3afe2f0c | ||
|
9ef1801ed6 | ||
|
893841bfbd | ||
|
338fff35ac | ||
|
138110f596 | ||
|
ec1f34ccea | ||
|
643fa18cae | ||
|
bc10ca501b | ||
|
cb32326fe6 | ||
|
30f02345a8 | ||
|
b30ddf90d2 | ||
|
d6914d79a1 | ||
|
5e6970b615 | ||
|
6791c0b208 | ||
|
857d65c1ce | ||
|
735d9f049c | ||
|
6c47a40730 | ||
|
28382a47d3 | ||
|
6b89dd7db9 | ||
|
763f64b653 | ||
|
e77fa175aa | ||
|
7cb1452d29 | ||
|
fd7d9622e3 | ||
|
784b8ee74c | ||
|
8e0dcd212d | ||
|
8e073d10d7 | ||
|
0d719bbdf3 | ||
|
b92e1f9574 | ||
|
30c7db3f25 | ||
|
fff333f89b | ||
|
092e9fb4cc | ||
|
b2242c571f | ||
|
e9ac53bb9a | ||
|
99a6f8dbc6 | ||
|
e5d264caf0 | ||
|
f7ec679fec | ||
|
fb744a338c | ||
|
cfc2b55e05 | ||
|
33f33ed4e3 | ||
|
93f5640a2d | ||
|
31aaf207d6 | ||
|
18f016cba7 | ||
|
36bdc0be1a | ||
|
d3572e1b81 | ||
|
420275793e | ||
|
bb898fa2e2 | ||
|
bd74c4e577 | ||
|
a820b8ce7b | ||
|
e19a07d400 | ||
|
50b02d41a0 | ||
|
eba2ded88a | ||
|
36abf7457f | ||
|
b8a035d2da | ||
|
20b142e8e3 | ||
|
05beb87852 | ||
|
1cd1582506 | ||
|
6f9cf6c70d | ||
|
a280dcda23 | ||
|
56ba6215a2 | ||
|
21a4095a99 | ||
|
c7583f1227 | ||
|
9da6ce474c | ||
|
99b5212550 | ||
|
95c8031f3d | ||
|
a146e53eb0 | ||
|
ae123d8f14 | ||
|
e1438774af | ||
|
b632dce0da | ||
|
a373682fa4 | ||
|
ec334bdd36 | ||
|
eb5e2251bd | ||
|
f08cb4ad56 | ||
|
db441a64b1 | ||
|
b55746b1e1 | ||
|
a79603e4ca | ||
|
00c2b55b56 | ||
|
5c8ec4504b | ||
|
460565056e | ||
|
2459648574 | ||
|
6e8585e88c | ||
|
858c086eee | ||
|
731e6752eb | ||
|
26b9541bca | ||
|
b9f184fda4 | ||
|
0b12d706e3 | ||
|
29bf51d25a | ||
|
c2ced4cd59 | ||
|
f1168187c9 | ||
|
2d8d420949 | ||
|
f09b4bd4f4 | ||
|
df6e3f8da9 | ||
|
3f785472c4 | ||
|
f11397c31a | ||
|
eb070b9652 | ||
|
ed7c327b48 | ||
|
4e0e50ae27 | ||
|
e4fbca59b4 | ||
|
6151c52824 | ||
|
aac7ad3e14 | ||
|
5a91b52ef4 | ||
|
b4f970ee73 | ||
|
e9194cbf4a | ||
|
38d967c414 | ||
|
b74e7e7353 | ||
|
37f0d97159 | ||
|
f35c9f25f0 | ||
|
1bb8555691 | ||
|
29219500b7 | ||
|
d817fe8e14 | ||
|
f81827c151 | ||
|
15fad17f37 | ||
|
a6531c840b | ||
|
85ee2fc65a | ||
|
3062c190bb | ||
|
cc310f71cc | ||
|
58adfcd514 | ||
|
d7f32d47ba | ||
|
b6926d9ab4 | ||
|
012b4adec7 | ||
|
02091267bf | ||
|
0d4b6addcc | ||
|
eb7e3250d3 | ||
|
1841015428 | ||
|
96f75eae4d | ||
|
bab7dbcaef | ||
|
88d4cd0970 | ||
|
4e7159c22c | ||
|
767096b9bb | ||
|
1ad83889be | ||
|
fc55e3a3df | ||
|
bb397cc668 | ||
|
271725faa5 | ||
|
deedf6e8b6 | ||
|
6cb7fb6d52 | ||
|
883a9f22e2 | ||
|
d06d06e050 | ||
|
48d70280eb | ||
|
c58d94f3fd | ||
|
59d45a5440 | ||
|
0d8d236be6 | ||
|
82f0a49062 | ||
|
4617da2818 | ||
|
8f9f44b9e8 | ||
|
2f695ef980 | ||
|
c0996923c6 | ||
|
3f38835105 | ||
|
2f77a6bf5a | ||
|
8562a4c986 | ||
|
82e0675c07 | ||
|
743637ebda | ||
|
1bcfc9a5cc | ||
|
69c81aa50d | ||
|
c47e30e960 | ||
|
be14aca075 | ||
|
c6c8cabdaf | ||
|
190dcef155 | ||
|
e8bf0fc099 | ||
|
e680fd27e7 | ||
|
883843a72d | ||
|
f72b1f8c45 | ||
|
33ca29f395 | ||
|
b2344a852e | ||
|
126133ead7 | ||
|
d6c2b1121c | ||
|
b0cf47cc2b | ||
|
9758c12176 | ||
|
23fe47bdcb | ||
|
17271eae67 | ||
|
8d63c49ba1 | ||
|
2e457c394f | ||
|
6f7247ca13 | ||
|
9c067562fa | ||
|
80fc8c286e | ||
|
6922160423 | ||
|
668f627532 | ||
|
5345ac785b | ||
|
09cf0b7af3 | ||
|
75d8de93ae | ||
|
570f769744 | ||
|
fe61d2fd67 | ||
|
dadae12253 | ||
|
4969bfdb66 | ||
|
cf53026b51 | ||
|
8f83c805b1 | ||
|
ddceb295f4 | ||
|
f4b8bce837 | ||
|
60b40fdc99 | ||
|
ebe0d4f47e | ||
|
b72bd0b2b5 | ||
|
457d6f616a | ||
|
f5c80a6d75 | ||
|
945dfabd71 | ||
|
ad3a0a184f | ||
|
558abf648b | ||
|
76df374624 | ||
|
df90c478e2 | ||
|
979edeb213 | ||
|
e7c400a0aa | ||
|
9dc9305d93 | ||
|
265c08661c | ||
|
629153582f | ||
|
4a24392c9c | ||
|
de8f06b512 | ||
|
1beeaf9db5 | ||
|
96b36d0f9e | ||
|
578ea4abb8 | ||
|
f34407bfda | ||
|
74dd79e97f | ||
|
64097983f1 | ||
|
4777f5d787 | ||
|
f436f20eb8 | ||
|
f79b94111f | ||
![]() |
36c3160c39 | ||
|
8daf798e57 | ||
|
3a00f3398c | ||
|
71c108e0e1 | ||
|
5d16f431b3 | ||
|
4337daddb2 | ||
|
04ec587230 | ||
|
4d7979aa77 | ||
|
a244f93243 | ||
|
5f89fab9d7 | ||
|
fbfd76023b | ||
|
ce6806bbf4 | ||
|
3efede2662 | ||
|
9a73c3a88d | ||
|
8f0b59a4b2 | ||
|
8edb04fddd | ||
|
f1e20a61f1 | ||
|
95880ca74b | ||
|
8e4741ad77 | ||
|
7d9378752a | ||
|
fbc9deb424 | ||
|
1d71f78f90 | ||
|
fb4a7968ca | ||
|
4e9e051caa | ||
|
97c308076f | ||
|
36e617ae70 | ||
|
679d758627 | ||
|
f719ba3f4e | ||
|
ad500441af | ||
|
e8316b2087 | ||
|
57aed1d5c6 | ||
|
282ae1dc9e | ||
|
07e62ae5da | ||
|
cf3ca44210 | ||
|
e953733323 | ||
|
8e83f3632c | ||
|
6c8fdfefbe | ||
|
38a35ab639 | ||
|
f5228695a2 | ||
|
acfc815e1d | ||
|
fb8dc41f6d | ||
|
58a7be0a31 | ||
|
b4a8471aa1 | ||
|
abdae3a7ec | ||
|
ce45ecc235 | ||
|
9a7f7bfdf5 | ||
|
c2dad29429 | ||
|
ad13b466d2 | ||
|
9df3fb13c7 | ||
|
ca213922d0 | ||
|
9a06e481b7 | ||
|
8f52604987 | ||
|
28fd9feb40 | ||
|
0030bcbd33 | ||
|
7b892eb3e1 | ||
|
12292126dc | ||
|
9a875864cf | ||
|
6d46833eb2 | ||
|
f5186f31f1 | ||
|
471b695331 | ||
|
98f20e4cc1 | ||
|
69f20f981a | ||
|
713ed3aca1 | ||
|
0bdf1f23b5 | ||
|
7f4ca71125 | ||
|
63fc539a25 | ||
|
3124450aff | ||
|
b0515c1dd5 | ||
|
1797e15b8c | ||
|
8957883a88 | ||
|
1ef7d09ce8 | ||
|
f9165a5e01 | ||
|
92af1b1c12 | ||
|
7016221556 | ||
|
bc611cf51c | ||
|
a30553ddbb | ||
|
91d9d65a03 | ||
|
7bb6411dfc | ||
|
f14009601e | ||
|
03da887339 | ||
|
fd36510807 | ||
|
4722695800 | ||
|
6e784e29de | ||
|
e2ec092aec | ||
|
7cea737115 | ||
|
04086a90c9 | ||
|
90690702e1 | ||
|
35760ac68f | ||
|
b8b0d97525 | ||
|
b5b17ed4d8 | ||
|
d50ff9b5d9 | ||
|
32f7ca261f | ||
|
6ca5e6184f | ||
|
11495b48ee | ||
|
b3106738eb | ||
|
f73bacb454 | ||
|
5de404ddd9 | ||
|
c96492b956 | ||
|
d6b20d3e79 | ||
|
1787377450 | ||
|
a508b007d8 | ||
|
142dc1e962 | ||
|
0cbd666875 | ||
|
361be7f6b7 | ||
|
b5cd9923f2 | ||
|
4758e2ff75 | ||
|
7f9e5765d2 | ||
|
bad1bab9e8 | ||
|
dba538eb4d | ||
|
351d90c339 | ||
|
de71776e02 | ||
|
e879982cfd | ||
|
c72739e966 | ||
|
9b8f86f843 | ||
|
84f1a936db | ||
|
992ca76afc | ||
|
d1bf3f02c7 | ||
|
e6aa25a103 | ||
|
e45497dfd6 | ||
|
d15d1156bd | ||
|
7ab6ed7ef9 | ||
|
0a83024505 | ||
|
7b278755fc | ||
|
775500cf1f | ||
|
5e018965ee | ||
|
6699329d3f | ||
|
a4e833e860 | ||
|
dd5edbc6ee | ||
|
a008b8b541 | ||
|
6632ce5fb0 | ||
|
89b1ac6eac | ||
|
cae4463e83 | ||
|
cc529a1803 | ||
|
08faaaf623 | ||
|
f7a8b6983c | ||
|
3e3f426af8 | ||
|
3d26eb79c2 | ||
|
ed30b487d6 | ||
|
387fc0be26 | ||
|
48b3b78a38 | ||
|
fce89d0ffe | ||
|
b329003c8f | ||
|
d0dc7cebf9 | ||
|
f7dfd65374 | ||
|
fbc25f8d3d | ||
|
2bbd395d5d | ||
|
e7df33d752 | ||
|
bbc8f837d7 | ||
|
a848306b89 | ||
|
7c4dba29c7 | ||
|
07ef59af78 | ||
|
1a0effa961 | ||
|
2116fbb3c2 | ||
|
8d6f86b317 | ||
|
8311500ac0 | ||
|
43de28ae15 | ||
|
60ac4faca0 | ||
|
9116afaf59 | ||
|
0b663ca82a | ||
|
e4de2132e9 | ||
|
dc094df214 | ||
|
5ca98b113e | ||
|
986f01237f | ||
|
bf8f83decc | ||
|
d60f3b89a7 | ||
|
0d620b7701 | ||
|
5af4159848 | ||
|
d96ced8e2d | ||
|
e1062a02d1 | ||
|
9ab7c47dc7 | ||
|
649d72e7de | ||
|
e98471d5b6 | ||
|
fb9cc0262b | ||
|
26de688e68 | ||
|
eb88660074 | ||
|
3a327e08e9 | ||
|
2ff0e48142 | ||
|
74d01a6957 | ||
|
aecc3ad195 | ||
|
8426bf953c | ||
|
7163e1d8b6 | ||
|
bf74e6e7d4 | ||
|
8caa66be27 | ||
|
89a6b8a9a1 | ||
|
f74ab463d4 | ||
|
c7b170da6d | ||
|
4b86c2ff98 | ||
|
b1f89699b4 | ||
|
62474d0e75 | ||
|
8fcf7ccaa7 | ||
|
d78ae3c852 | ||
|
2ec0a43c4a | ||
|
26ed518fbe | ||
|
c04dff99da | ||
|
938177e225 | ||
|
36e7ac2048 | ||
|
453d2d5bb0 | ||
|
773a23630b | ||
|
9cf61fb82c | ||
|
c822ea753e | ||
|
8c8c5bb766 | ||
|
972b9520bf | ||
|
68c8ebcd77 | ||
|
eb4f55c60b | ||
|
9ca65c9f18 | ||
|
441c6730e9 | ||
|
6f9de4130f | ||
|
0d7e236039 | ||
|
7bb1b81e79 | ||
|
95988e8e25 | ||
|
68f3c9c4c6 | ||
|
5a9527894a | ||
|
a14b39555e | ||
|
0f2e20f5e5 | ||
|
3702b10997 | ||
|
514ccf8a72 | ||
|
232d4873ba | ||
|
aa29849976 | ||
|
6ba2461445 | ||
|
09707f29fc | ||
|
430ab6e444 | ||
|
5185d6a0f3 | ||
|
3daa9c2cc8 | ||
|
d6c3da21f7 | ||
|
293314405f | ||
|
ba06d458d3 | ||
|
e71978456a | ||
|
c13ef73e8b | ||
|
7715486906 | ||
|
65ff3744a0 | ||
|
c688452d7b | ||
|
69ed631d44 | ||
|
f0381a7fb3 | ||
|
ffa46898d9 | ||
|
27717bc70c | ||
|
895eaef99b | ||
|
f737c6da24 | ||
|
5696afcadd | ||
|
a88aae5e04 | ||
|
8032428428 | ||
|
e06a4d1ce4 | ||
|
cdb7acf6c2 | ||
|
e189c22bb0 | ||
|
c1b9f66525 | ||
|
98160406f7 | ||
|
9b84b6e403 | ||
|
f942fc3b68 | ||
|
a7694e0112 | ||
|
48145ba51e | ||
|
f5d3164908 | ||
|
c59ff69473 | ||
|
cdee847cea | ||
|
34bdfe9416 | ||
|
749722fca6 | ||
|
05a2a619c1 | ||
|
4268556edb | ||
|
69590bcf72 | ||
|
a56eef0fec | ||
|
114a759d2c | ||
|
65cac922b3 | ||
|
e55dfa7551 | ||
|
d575f45411 | ||
|
31b0b7f580 | ||
|
7c8799e493 | ||
|
ef987ae2ec | ||
|
14fb7e13fb | ||
|
e92ea48e88 | ||
|
f87ddf1056 | ||
|
33ad332ce9 | ||
|
ed045b45ba | ||
|
5a974fe72d | ||
|
d1eb187e26 | ||
|
2144109dd7 | ||
|
1f60e28204 | ||
|
561c3641c6 | ||
|
4293dcb254 | ||
|
6c145bbb23 | ||
|
2cd517c099 | ||
|
0b5409c53d | ||
|
60b32a9fed | ||
|
7756081fdb | ||
|
369ed9bc2e | ||
|
31e3b6fbcb | ||
|
6b25b4df22 | ||
|
6943041e0f | ||
|
9a733bb3c8 | ||
|
c12ba14dda | ||
|
f8fac432c6 | ||
|
5eec3a8867 | ||
|
ff0dc44cd7 | ||
|
287f3ea99a | ||
|
97f1f3dcf4 | ||
|
a1f9be3089 | ||
|
be0df0669e | ||
|
f9abee391d | ||
|
e648de560d | ||
|
67570a0a1d | ||
|
dd0bd29b68 | ||
|
2995c50b1c | ||
|
a33f49ff4b | ||
|
805c5ad0cd | ||
|
25ed33329b | ||
|
e404f5098a | ||
|
5077f1dc6a | ||
|
f0a857ddde | ||
|
1747eb2745 | ||
|
267e242238 | ||
|
fab6d79e84 | ||
|
62725a76ef | ||
|
cdd34e1e4b | ||
|
505588d585 | ||
|
826bc374b1 | ||
|
811d8b90d7 | ||
|
80a8a57471 | ||
|
a99aa1dd61 |
35
.github/workflows/build.yml
vendored
@ -11,33 +11,24 @@ jobs:
|
||||
- name: Install cargo-deb
|
||||
run: cargo install cargo-deb
|
||||
- uses: actions/checkout@master
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
export DEBIAN_FRONTED=noninteractive
|
||||
sudo apt-get -qq update
|
||||
sudo apt-get install -y libxkbcommon-dev
|
||||
- name: Enable Link Time Optimizations
|
||||
run: |
|
||||
echo "[profile.release]" >> Cargo.toml
|
||||
echo "lto = true" >> Cargo.toml
|
||||
- name: Add .deb package metadata
|
||||
run: |
|
||||
echo '[package.metadata.deb.variants.todos]' >> Cargo.toml
|
||||
echo 'extended-description = "A simple Todo app built with Iced, a cross-platform GUI library for Rust"' >> Cargo.toml
|
||||
echo 'assets = [' >> Cargo.toml
|
||||
echo '["target/release/examples/todos", "usr/bin/iced-todos", "755"],' >> Cargo.toml
|
||||
echo '["iced-todos.desktop", "usr/share/applications/", "644"]' >> Cargo.toml
|
||||
echo ']' >> Cargo.toml
|
||||
- name: Create .desktop file
|
||||
run: |
|
||||
echo '[Desktop Entry]' >> iced-todos.desktop
|
||||
echo 'Name=Todos - Iced' >> iced-todos.desktop
|
||||
echo 'Exec=iced-todos' >> iced-todos.desktop
|
||||
echo 'Type=Application' >> iced-todos.desktop
|
||||
- name: Build todos binary
|
||||
run: cargo build --verbose --release --example todos
|
||||
run: cargo build --verbose --release --package todos
|
||||
- name: Archive todos binary
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: todos-x86_64-unknown-linux-gnu
|
||||
path: target/release/examples/todos
|
||||
- name: Build todos .deb package
|
||||
run: cargo deb --variant todos -- --example todos
|
||||
path: target/release/todos
|
||||
- name: Pack todos .deb package
|
||||
run: cargo deb --no-build --package todos
|
||||
- name: Rename todos .deb package
|
||||
run: mv target/debian/*.deb target/debian/iced_todos-x86_64-debian-linux-gnu.deb
|
||||
- name: Archive todos .deb package
|
||||
@ -61,12 +52,12 @@ jobs:
|
||||
echo '[target.x86_64-pc-windows-msvc]' >> .cargo/config
|
||||
echo 'rustflags = ["-Ctarget-feature=+crt-static"]' >> .cargo/config
|
||||
- name: Build todos binary
|
||||
run: cargo build --verbose --release --example todos
|
||||
run: cargo build --verbose --release --package todos
|
||||
- name: Archive todos binary
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: todos-x86_64-pc-windows-msvc
|
||||
path: target/release/examples/todos.exe
|
||||
path: target/release/todos.exe
|
||||
|
||||
todos_macos:
|
||||
runs-on: macOS-latest
|
||||
@ -80,9 +71,9 @@ jobs:
|
||||
- name: Build todos binary
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: 10.14
|
||||
run: cargo build --verbose --release --example todos
|
||||
run: cargo build --verbose --release --package todos
|
||||
- name: Archive todos binary
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: todos-x86_64-apple-darwin
|
||||
path: target/release/examples/todos
|
||||
path: target/release/todos
|
||||
|
12
.github/workflows/format.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
name: Format
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
all:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: hecrj/setup-rust-action@v1
|
||||
with:
|
||||
components: rustfmt
|
||||
- uses: actions/checkout@master
|
||||
- name: Check format
|
||||
run: cargo fmt --all -- --check
|
23
.github/workflows/test.yml
vendored
@ -1,7 +1,7 @@
|
||||
name: Test
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
all:
|
||||
native:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@ -12,7 +12,28 @@ jobs:
|
||||
with:
|
||||
rust-version: ${{ matrix.rust }}
|
||||
- uses: actions/checkout@master
|
||||
- name: Install dependencies
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
export DEBIAN_FRONTED=noninteractive
|
||||
sudo apt-get -qq update
|
||||
sudo apt-get install -y libxkbcommon-dev
|
||||
- name: Run tests
|
||||
run: |
|
||||
cargo test --verbose --all
|
||||
cargo test --verbose --all --all-features
|
||||
|
||||
web:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: hecrj/setup-rust-action@v1
|
||||
with:
|
||||
rust-version: stable
|
||||
targets: wasm32-unknown-unknown
|
||||
- uses: actions/checkout@master
|
||||
- name: Run checks
|
||||
run: cargo check --package iced --target wasm32-unknown-unknown
|
||||
- name: Check compilation of `tour` example
|
||||
run: cargo build --package tour --target wasm32-unknown-unknown
|
||||
- name: Check compilation of `todos` example
|
||||
run: cargo build --package todos --target wasm32-unknown-unknown
|
||||
|
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
/target
|
||||
target/
|
||||
pkg/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
.cargo/
|
||||
|
15
.travis.yml
@ -1,15 +0,0 @@
|
||||
language: rust
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
fast_finish: true
|
||||
before_install:
|
||||
- |
|
||||
if [[ "${TRAVIS_OS_NAME}" == "linux" ]]; then
|
||||
sudo apt-get -qq update
|
||||
sudo apt-get install -y libasound2-dev libudev-dev
|
||||
fi
|
210
CHANGELOG.md
@ -6,6 +6,210 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.3.0] - 2021-03-31
|
||||
### Added
|
||||
- Touch support. [#57] [#650] (thanks to @simlay and @discordance!)
|
||||
- Clipboard write access for
|
||||
- `TextInput` widget. [#770]
|
||||
- `Application::update`. [#773]
|
||||
- `image::Viewer` widget. It allows panning and scaling of an image. [#319] (thanks to @tarkah!)
|
||||
- `Tooltip` widget. It annotates content with some text on mouse hover. [#465] (thanks to @yusdacra!)
|
||||
- Support for the [`smol`] async runtime. [#699] (thanks to @JayceFayne!)
|
||||
- Support for graceful exiting when using the `Application` trait. [#804]
|
||||
- Image format features in [`iced_wgpu`] to reduce code bloat. [#392] (thanks to @unrelentingtech!)
|
||||
- `Focused` and `Unfocused` variant to `window::Event`. [#701] (thanks to @cossonleo!)
|
||||
- `WGPU_BACKEND` environment variable to configure the internal graphics backend of `iced_wgpu`. [#789] (thanks to @Cupnfish!)
|
||||
|
||||
### Changed
|
||||
- The `TitleBar` of a `PaneGrid` now supports generic elements. [#657] (thanks to @clarkmoody!)
|
||||
- The `Error` type now implements `Send` and `Sync`. [#719] (thanks to @taiki-e!)
|
||||
- The `Style` types in `iced_style` now implement `Clone` and `Copy`. [#720] (thanks to @taiki-e!)
|
||||
- The following dependencies have been updated:
|
||||
- [`font-kit`] → `0.10` [#669]
|
||||
- [`glutin`] → `0.26` [#658]
|
||||
- [`resvg`] → `0.12` [#669]
|
||||
- [`tokio`] → `1.0` [#672] (thanks to @yusdacra!)
|
||||
- [`winit`] → `0.24` [#658]
|
||||
- [`wgpu`] → `0.7` [#725] (thanks to @PolyMeilex)
|
||||
- The following examples were improved:
|
||||
- `download_progress` now showcases multiple file downloads at once. [#283] (thanks to @Folyd!)
|
||||
- `solar_system` uses the new `rand` API. [#760] (thanks to @TriedAngle!)
|
||||
|
||||
### Fixed
|
||||
- Button events not being propagated to contents. [#668]
|
||||
- Incorrect overlay implementation for the `Button` widget. [#764]
|
||||
- `Viewport::physical_width` returning the wrong value. [#700]
|
||||
- Outdated documentation for the `Sandbox` trait. [#710]
|
||||
|
||||
[#57]: https://github.com/hecrj/iced/pull/57
|
||||
[#283]: https://github.com/hecrj/iced/pull/283
|
||||
[#319]: https://github.com/hecrj/iced/pull/319
|
||||
[#392]: https://github.com/hecrj/iced/pull/392
|
||||
[#465]: https://github.com/hecrj/iced/pull/465
|
||||
[#650]: https://github.com/hecrj/iced/pull/650
|
||||
[#657]: https://github.com/hecrj/iced/pull/657
|
||||
[#658]: https://github.com/hecrj/iced/pull/658
|
||||
[#668]: https://github.com/hecrj/iced/pull/668
|
||||
[#669]: https://github.com/hecrj/iced/pull/669
|
||||
[#672]: https://github.com/hecrj/iced/pull/672
|
||||
[#699]: https://github.com/hecrj/iced/pull/699
|
||||
[#700]: https://github.com/hecrj/iced/pull/700
|
||||
[#701]: https://github.com/hecrj/iced/pull/701
|
||||
[#710]: https://github.com/hecrj/iced/pull/710
|
||||
[#719]: https://github.com/hecrj/iced/pull/719
|
||||
[#720]: https://github.com/hecrj/iced/pull/720
|
||||
[#725]: https://github.com/hecrj/iced/pull/725
|
||||
[#760]: https://github.com/hecrj/iced/pull/760
|
||||
[#764]: https://github.com/hecrj/iced/pull/764
|
||||
[#770]: https://github.com/hecrj/iced/pull/770
|
||||
[#773]: https://github.com/hecrj/iced/pull/773
|
||||
[#789]: https://github.com/hecrj/iced/pull/789
|
||||
[#804]: https://github.com/hecrj/iced/pull/804
|
||||
[`smol`]: https://github.com/smol-rs/smol
|
||||
[`winit`]: https://github.com/rust-windowing/winit
|
||||
[`glutin`]: https://github.com/rust-windowing/glutin
|
||||
[`font-kit`]: https://github.com/servo/font-kit
|
||||
|
||||
## [0.2.0] - 2020-11-26
|
||||
### Added
|
||||
- __[`Canvas` interactivity][canvas]__ (#325)
|
||||
A trait-based approach to react to mouse and keyboard interactions in [the `Canvas` widget][#193].
|
||||
|
||||
- __[`iced_graphics` subcrate][opengl]__ (#354)
|
||||
A backend-agnostic graphics subcrate that can be leveraged to build new renderers.
|
||||
|
||||
- __[OpenGL renderer][opengl]__ (#354)
|
||||
An OpenGL renderer powered by [`iced_graphics`], [`glow`], and [`glutin`]. It is an alternative to the default [`wgpu`] renderer.
|
||||
|
||||
- __[Overlay support][pick_list]__ (#444)
|
||||
Basic support for superpositioning interactive widgets on top of other widgets.
|
||||
|
||||
- __[Faster event loop][view]__ (#597)
|
||||
The event loop now takes advantage of the data dependencies in [The Elm Architecture] and leverages the borrow checker to keep the widget tree alive between iterations, avoiding unnecessary rebuilds.
|
||||
|
||||
- __[Event capturing][event]__ (#614)
|
||||
The runtime now can tell whether a widget has handled an event or not, easing [integration with existing applications].
|
||||
|
||||
- __[`PickList` widget][pick_list]__ (#444)
|
||||
A drop-down selector widget built on top of the new overlay support.
|
||||
|
||||
- __[`QRCode` widget][qr_code]__ (#622)
|
||||
A widget that displays a QR code, powered by [the `qrcode` crate].
|
||||
|
||||
[canvas]: https://github.com/hecrj/iced/pull/325
|
||||
[opengl]: https://github.com/hecrj/iced/pull/354
|
||||
[`iced_graphics`]: https://github.com/hecrj/iced/pull/354
|
||||
[pane_grid]: https://github.com/hecrj/iced/pull/397
|
||||
[pick_list]: https://github.com/hecrj/iced/pull/444
|
||||
[error]: https://github.com/hecrj/iced/pull/514
|
||||
[view]: https://github.com/hecrj/iced/pull/597
|
||||
[event]: https://github.com/hecrj/iced/pull/614
|
||||
[color]: https://github.com/hecrj/iced/pull/200
|
||||
[qr_code]: https://github.com/hecrj/iced/pull/622
|
||||
[#193]: https://github.com/hecrj/iced/pull/193
|
||||
[`glutin`]: https://github.com/rust-windowing/glutin
|
||||
[`wgpu`]: https://github.com/gfx-rs/wgpu-rs
|
||||
[`glow`]: https://github.com/grovesNL/glow
|
||||
[the `qrcode` crate]: https://docs.rs/qrcode/0.12.0/qrcode/
|
||||
[integration with existing applications]: https://github.com/hecrj/iced/pull/183
|
||||
[The Elm Architecture]: https://guide.elm-lang.org/architecture/
|
||||
|
||||
|
||||
## [0.1.1] - 2020-04-15
|
||||
### Added
|
||||
- `Settings::with_flags` to easily initialize some default settings with flags. [#266]
|
||||
- `Default` implementation for `canvas::layer::Cache`. [#267]
|
||||
- `Ctrl + Del` support for `TextInput`. [#268]
|
||||
- Helper methods in `canvas::Path` to easily draw lines, rectangles, and circles. [#293]
|
||||
- `From<Color>` implementation for `canvas::Fill`. [#293]
|
||||
- `From<String>` implementation for `canvas::Text`. [#293]
|
||||
- `From<&str>` implementation for `canvas::Text`. [#293]
|
||||
|
||||
### Changed
|
||||
- `new` method of `Radio` and `Checkbox` now take a generic `Into<String>` for the label. [#260]
|
||||
- `Frame::fill` now takes a generic `Into<canvas::Fill>`. [#293]
|
||||
- `Frame::stroke` now takes a generic `Into<canvas::Stroke>`. [#293]
|
||||
- `Frame::fill_text` now takes a generic `Into<canvas::Text>`. [#293]
|
||||
|
||||
### Fixed
|
||||
- Feature flags not being referenced in documentation. [#259]
|
||||
- Crash in some graphics drivers when displaying an empty `Canvas`. [#278]
|
||||
- Text measuring when spaces where present at the beginning of a `TextInput` value. [#279]
|
||||
- `TextInput` producing a `Clip` primitive when unnecessary. [#279]
|
||||
- Alignment of `Text` primitive in `iced_wgpu`. [#281]
|
||||
- `CursorEntered` and `CursorLeft` not being generated. [#289]
|
||||
|
||||
### Removed
|
||||
- Unnecessary `'static` lifetimes in `Renderer` bounds. [#290]
|
||||
|
||||
[#259]: https://github.com/hecrj/iced/pull/259
|
||||
[#260]: https://github.com/hecrj/iced/pull/260
|
||||
[#266]: https://github.com/hecrj/iced/pull/266
|
||||
[#267]: https://github.com/hecrj/iced/pull/267
|
||||
[#268]: https://github.com/hecrj/iced/pull/268
|
||||
[#278]: https://github.com/hecrj/iced/pull/278
|
||||
[#279]: https://github.com/hecrj/iced/pull/279
|
||||
[#281]: https://github.com/hecrj/iced/pull/281
|
||||
[#289]: https://github.com/hecrj/iced/pull/289
|
||||
[#290]: https://github.com/hecrj/iced/pull/290
|
||||
[#293]: https://github.com/hecrj/iced/pull/293
|
||||
|
||||
|
||||
## [0.1.0] - 2020-04-02
|
||||
### Added
|
||||
- __[Event subscriptions]__ (#122)
|
||||
A declarative way to listen to external events asynchronously by leveraging [streams].
|
||||
|
||||
- __[Custom styling]__ (#146)
|
||||
A simple, trait-based approach for customizing the appearance of different widgets.
|
||||
|
||||
- __[`Canvas` widget]__ (#193)
|
||||
A widget for drawing 2D graphics with an interface inspired by the [Web Canvas API] and powered by [`lyon`].
|
||||
|
||||
- __[`PaneGrid` widget]__ (#224)
|
||||
A widget that dynamically organizes layout by splitting panes that can be resized and drag and dropped.
|
||||
|
||||
- __[`Svg` widget]__ (#111)
|
||||
A widget that renders vector graphics on top of [`resvg`] and [`raqote`]. Thanks to @Maldela!
|
||||
|
||||
- __[`ProgressBar` widget]__ (#141)
|
||||
A widget to notify progress of asynchronous tasks to your users. Thanks to @Songtronix!
|
||||
|
||||
- __[Configurable futures executor]__ (#164)
|
||||
Support for plugging [`tokio`], [`async-std`], [`wasm-bindgen-futures`], or your own custom futures executor to an application.
|
||||
|
||||
- __[Compatibility with existing `wgpu` projects]__ (#183)
|
||||
A bunch of improvements to the flexibility of [`iced_wgpu`] to allow integration in existing codebases.
|
||||
|
||||
- __[Text selection for `TextInput`]__ (#202)
|
||||
Thanks to @FabianLars and @Finnerale!
|
||||
|
||||
- __[Texture atlas for `iced_wgpu`]__ (#154)
|
||||
An atlas on top of [`guillotiere`] for batching draw calls. Thanks to @Maldela!
|
||||
|
||||
[Event subscriptions]: https://github.com/hecrj/iced/pull/122
|
||||
[Custom styling]: https://github.com/hecrj/iced/pull/146
|
||||
[`Canvas` widget]: https://github.com/hecrj/iced/pull/193
|
||||
[`PaneGrid` widget]: https://github.com/hecrj/iced/pull/224
|
||||
[`Svg` widget]: https://github.com/hecrj/iced/pull/111
|
||||
[`ProgressBar` widget]: https://github.com/hecrj/iced/pull/141
|
||||
[Configurable futures executor]: https://github.com/hecrj/iced/pull/164
|
||||
[Compatibility with existing `wgpu` projects]: https://github.com/hecrj/iced/pull/183
|
||||
[Clipboard access]: https://github.com/hecrj/iced/pull/132
|
||||
[Texture atlas for `iced_wgpu`]: https://github.com/hecrj/iced/pull/154
|
||||
[Text selection for `TextInput`]: https://github.com/hecrj/iced/pull/202
|
||||
[`lyon`]: https://github.com/nical/lyon
|
||||
[`guillotiere`]: https://github.com/nical/guillotiere
|
||||
[Web Canvas API]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
|
||||
[streams]: https://docs.rs/futures/0.3.4/futures/stream/index.html
|
||||
[`tokio`]: https://github.com/tokio-rs/tokio
|
||||
[`async-std`]: https://github.com/async-rs/async-std
|
||||
[`wasm-bindgen-futures`]: https://github.com/rustwasm/wasm-bindgen/tree/master/crates/futures
|
||||
[`resvg`]: https://github.com/RazrFalcon/resvg
|
||||
[`raqote`]: https://github.com/jrmuizel/raqote
|
||||
[`iced_wgpu`]: https://github.com/hecrj/iced/tree/master/wgpu
|
||||
|
||||
|
||||
## [0.1.0-beta] - 2019-11-25
|
||||
### Changed
|
||||
- The old `iced` becomes `iced_native`. The current `iced` crate turns into a batteries-included, cross-platform GUI library.
|
||||
@ -15,6 +219,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
- First release! :tada:
|
||||
|
||||
[Unreleased]: https://github.com/hecrj/iced/compare/0.1.0-beta...HEAD
|
||||
[Unreleased]: https://github.com/hecrj/iced/compare/0.3.0...HEAD
|
||||
[0.3.0]: https://github.com/hecrj/iced/compare/0.2.0...0.3.0
|
||||
[0.2.0]: https://github.com/hecrj/iced/compare/0.1.1...0.2.0
|
||||
[0.1.1]: https://github.com/hecrj/iced/compare/0.1.0...0.1.1
|
||||
[0.1.0]: https://github.com/hecrj/iced/compare/0.1.0-beta...0.1.0
|
||||
[0.1.0-beta]: https://github.com/hecrj/iced/compare/0.1.0-alpha...0.1.0-beta
|
||||
[0.1.0-alpha]: https://github.com/hecrj/iced/releases/tag/0.1.0-alpha
|
||||
|
87
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "iced"
|
||||
version = "0.1.0-beta"
|
||||
version = "0.3.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "A cross-platform GUI library inspired by Elm"
|
||||
@ -12,8 +12,39 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"]
|
||||
categories = ["gui"]
|
||||
|
||||
[features]
|
||||
default = ["wgpu", "default_system_font"]
|
||||
# Enables the `iced_wgpu` renderer
|
||||
wgpu = ["iced_wgpu"]
|
||||
# Enables the `Image` widget
|
||||
image = ["iced_wgpu/image"]
|
||||
# Enables the `Svg` widget
|
||||
svg = ["iced_wgpu/svg"]
|
||||
# Enables the `Canvas` widget
|
||||
canvas = ["iced_wgpu/canvas"]
|
||||
# Enables the `QRCode` widget
|
||||
qr_code = ["iced_wgpu/qr_code"]
|
||||
# Enables using system fonts
|
||||
default_system_font = ["iced_wgpu/default_system_font"]
|
||||
# Enables the `iced_glow` renderer. Overrides `iced_wgpu`
|
||||
glow = ["iced_glow", "iced_glutin"]
|
||||
# Enables the `Canvas` widget for `iced_glow`
|
||||
glow_canvas = ["iced_glow/canvas"]
|
||||
# Enables the `QRCode` widget for `iced_glow`
|
||||
glow_qr_code = ["iced_glow/qr_code"]
|
||||
# Enables using system fonts for `iced_glow`
|
||||
glow_default_system_font = ["iced_glow/default_system_font"]
|
||||
# Enables a debug view in native platforms (press F12)
|
||||
debug = ["iced_winit/debug"]
|
||||
# Enables `tokio` as the `executor::Default` on native platforms
|
||||
tokio = ["iced_futures/tokio"]
|
||||
# Enables old `tokio` (0.2) as the `executor::Default` on native platforms
|
||||
tokio_old = ["iced_futures/tokio_old"]
|
||||
# Enables `async-std` as the `executor::Default` on native platforms
|
||||
async-std = ["iced_futures/async-std"]
|
||||
# Enables `smol` as the `executor::Default` on native platforms
|
||||
smol = ["iced_futures/smol"]
|
||||
# Enables advanced color conversion via `palette`
|
||||
palette = ["iced_core/palette"]
|
||||
|
||||
[badges]
|
||||
maintenance = { status = "actively-developed" }
|
||||
@ -21,24 +52,56 @@ maintenance = { status = "actively-developed" }
|
||||
[workspace]
|
||||
members = [
|
||||
"core",
|
||||
"futures",
|
||||
"graphics",
|
||||
"glow",
|
||||
"glutin",
|
||||
"native",
|
||||
"style",
|
||||
"web",
|
||||
"wgpu",
|
||||
"winit",
|
||||
"examples/bezier_tool",
|
||||
"examples/clock",
|
||||
"examples/color_palette",
|
||||
"examples/counter",
|
||||
"examples/custom_widget",
|
||||
"examples/download_progress",
|
||||
"examples/events",
|
||||
"examples/game_of_life",
|
||||
"examples/geometry",
|
||||
"examples/integration",
|
||||
"examples/pane_grid",
|
||||
"examples/pick_list",
|
||||
"examples/pokedex",
|
||||
"examples/progress_bar",
|
||||
"examples/qr_code",
|
||||
"examples/scrollable",
|
||||
"examples/solar_system",
|
||||
"examples/stopwatch",
|
||||
"examples/styling",
|
||||
"examples/svg",
|
||||
"examples/todos",
|
||||
"examples/tour",
|
||||
"examples/tour_glow",
|
||||
"examples/tooltip",
|
||||
"examples/url_handler",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
iced_core = { version = "0.4", path = "core" }
|
||||
iced_futures = { version = "0.3", path = "futures" }
|
||||
thiserror = "1.0"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
iced_winit = { version = "0.1.0-alpha", path = "winit" }
|
||||
iced_wgpu = { version = "0.1.0", path = "wgpu" }
|
||||
iced_winit = { version = "0.3", path = "winit" }
|
||||
iced_glutin = { version = "0.2", path = "glutin", optional = true }
|
||||
iced_wgpu = { version = "0.4", path = "wgpu", optional = true }
|
||||
iced_glow = { version = "0.2", path = "glow", optional = true}
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
iced_web = { version = "0.1.0", path = "web" }
|
||||
iced_web = { version = "0.4", path = "web" }
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.7"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
directories = "2.0"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen = "0.2.51"
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
features = ["image", "svg", "canvas", "qr_code"]
|
||||
|
95
ECOSYSTEM.md
@ -1,10 +1,7 @@
|
||||
# Ecosystem
|
||||
This document describes the Iced ecosystem.
|
||||
|
||||
It quickly lists the different audiences of the library and explains how the different crates relate to each other.
|
||||
|
||||
## Users
|
||||
This document describes the Iced ecosystem and explains how the different crates relate to each other.
|
||||
|
||||
## Overview
|
||||
Iced is meant to be used by 2 different types of users:
|
||||
|
||||
- __End-users__. They should be able to:
|
||||
@ -18,71 +15,81 @@ Iced is meant to be used by 2 different types of users:
|
||||
- integrate existing runtimes in their own system (like game engines),
|
||||
- and create their own custom renderers.
|
||||
|
||||
## Crates
|
||||
Iced consists of different crates which offer different layers of abstractions for our users. This modular architecture helps us keep implementation details hidden and decoupled, which should allow us to rewrite or change strategies in the future.
|
||||
|
||||

|
||||
<p align="center">
|
||||
<img alt="The Iced Ecosystem" src="docs/graphs/ecosystem.png" width="60%">
|
||||
</p>
|
||||
|
||||
### [`iced_core`]
|
||||
## The foundations
|
||||
There are a bunch of concepts that permeate the whole ecosystem. These concepts are considered __the foundations__, and they are provided by three different crates:
|
||||
|
||||
[`iced_core`] holds basic reusable types of the public API. For instance, basic data types like `Point`, `Rectangle`, `Length`, etc.
|
||||
- [`iced_core`] contains many lightweight, reusable primitives (e.g. `Point`, `Rectangle`, `Color`).
|
||||
- [`iced_futures`] implements the concurrent concepts of [The Elm Architecture] on top of the [`futures`] ecosystem.
|
||||
- [`iced_style`] defines the default styling capabilities of built-in widgets.
|
||||
|
||||
This crate is meant to be a starting point for an Iced runtime.
|
||||
<p align="center">
|
||||
<img alt="The foundations" src="docs/graphs/foundations.png" width="50%">
|
||||
</p>
|
||||
|
||||
### [`iced_native`]
|
||||
[`iced_native`] takes [`iced_core`] and builds a native runtime on top of it, featuring:
|
||||
- A custom layout engine, greatly inspired by [`druid`]
|
||||
- Event handling for all the built-in widgets
|
||||
- A renderer-agnostic API
|
||||
## The native target
|
||||
The native side of the ecosystem is split into two different groups: __renderers__ and __shells__.
|
||||
|
||||
To achieve this, it introduces a bunch of reusable interfaces:
|
||||
- A `Widget` trait, which is used to implement new widgets: from layout requirements to event and drawing logic.
|
||||
- A bunch of `Renderer` traits, meant to keep the crate renderer-agnostic.
|
||||
- A `Windowed` trait, leveraging [`raw-window-handle`], which can be implemented by graphical renderers that target _windows_. Window-based shells (like [`iced_winit`]) can use this trait to stay renderer-agnostic.
|
||||
<p align="center">
|
||||
<img alt="The native target" src="docs/graphs/native.png" width="80%">
|
||||
</p>
|
||||
|
||||
[`druid`]: https://github.com/xi-editor/druid
|
||||
[`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle
|
||||
### Renderers
|
||||
The widgets of a _graphical_ user interface produce some primitives that eventually need to be drawn on screen. __Renderers__ take care of this task, potentially leveraging GPU acceleration.
|
||||
|
||||
### [`iced_web`]
|
||||
[`iced_web`] takes [`iced_core`] and builds a WebAssembly runtime on top. It achieves this by introducing a `Widget` trait that can be used to produce VDOM nodes.
|
||||
Currently, there are two different official renderers:
|
||||
|
||||
The crate is currently a simple abstraction layer over [`dodrio`].
|
||||
- [`iced_wgpu`] is powered by [`wgpu`] and supports Vulkan, DirectX 12, and Metal.
|
||||
- [`iced_glow`] is powered by [`glow`] and supports OpenGL 3.3+.
|
||||
|
||||
[`dodrio`]: https://github.com/fitzgen/dodrio
|
||||
Additionally, the [`iced_graphics`] subcrate contains a bunch of backend-agnostic types that can be leveraged to build renderers. Both of the renderers rely on the graphical foundations provided by this crate.
|
||||
|
||||
### [`iced_wgpu`]
|
||||
[`iced_wgpu`] is a [`wgpu`] renderer for [`iced_native`]. For now, it is the default renderer of Iced in native platforms.
|
||||
### Shells
|
||||
The widgets of a graphical user _interface_ are interactive. __Shells__ gather and process user interactions in an event loop.
|
||||
|
||||
[`wgpu`] supports most modern graphics backends: Vulkan, Metal, DX11, and DX12 (OpenGL and WebGL are still WIP). Additionally, it will support the incoming [WebGPU API].
|
||||
Normally, a shell will be responsible of creating a window and managing the lifecycle of a user interface, implementing a runtime of [The Elm Architecture].
|
||||
|
||||
Currently, [`iced_wgpu`] supports the following primitives:
|
||||
- Text, which is rendered using [`wgpu_glyph`]. No shaping at all.
|
||||
- Quads or rectangles, with rounded borders and a solid background color.
|
||||
- Images, lazily loaded from the filesystem.
|
||||
- Clip areas, useful to implement scrollables or hide overflowing content.
|
||||
As of now, there are two official shells:
|
||||
|
||||
[`wgpu`]: https://github.com/gfx-rs/wgpu-rs
|
||||
[WebGPU API]: https://gpuweb.github.io/gpuweb/
|
||||
[`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph
|
||||
- [`iced_winit`] implements a shell runtime on top of [`winit`].
|
||||
- [`iced_glutin`] is similar to [`iced_winit`], but it also deals with [OpenGL context creation].
|
||||
|
||||
### [`iced_winit`]
|
||||
[`iced_winit`] offers some convenient abstractions on top of [`iced_native`] to quickstart development when using [`winit`].
|
||||
## The web target
|
||||
The Web platform provides all the abstractions necessary to draw widgets and gather users interactions.
|
||||
|
||||
It exposes a renderer-agnostic `Application` trait that can be implemented and then run with a simple call. The use of this trait is optional. A `conversion` module is provided for users that decide to implement a custom event loop.
|
||||
Therefore, unlike the native path, the web side of the ecosystem does not need to split renderers and shells. Instead, [`iced_web`] leverages [`dodrio`] to both render widgets and implement a proper runtime.
|
||||
|
||||
[`winit`]: https://github.com/rust-windowing/winit
|
||||
|
||||
### [`iced`]
|
||||
## Iced
|
||||
Finally, [`iced`] unifies everything into a simple abstraction to create cross-platform applications:
|
||||
|
||||
- On native, it uses [`iced_winit`] and [`iced_wgpu`].
|
||||
- On native, it uses __[shells](#shells)__ and __[renderers](#renderers)__.
|
||||
- On the web, it uses [`iced_web`].
|
||||
|
||||
This is the crate meant to be used by __end-users__.
|
||||
<p align="center">
|
||||
<img alt="Iced" src="docs/graphs/iced.png" width="80%">
|
||||
</p>
|
||||
|
||||
[`iced_core`]: core
|
||||
[`iced_futures`]: futures
|
||||
[`iced_style`]: style
|
||||
[`iced_native`]: native
|
||||
[`iced_web`]: web
|
||||
[`iced_graphics`]: graphics
|
||||
[`iced_wgpu`]: wgpu
|
||||
[`iced_glow`]: glow
|
||||
[`iced_winit`]: winit
|
||||
[`iced_glutin`]: glutin
|
||||
[`iced`]: ..
|
||||
[`futures`]: https://github.com/rust-lang/futures-rs
|
||||
[`glow`]: https://github.com/grovesNL/glow
|
||||
[`wgpu`]: https://github.com/gfx-rs/wgpu-rs
|
||||
[`winit`]: https://github.com/rust-windowing/winit
|
||||
[`glutin`]: https://github.com/rust-windowing/glutin
|
||||
[`dodrio`]: https://github.com/fitzgen/dodrio
|
||||
[OpenGL context creation]: https://www.khronos.org/opengl/wiki/Creating_an_OpenGL_Context
|
||||
[The Elm Architecture]: https://guide.elm-lang.org/architecture/
|
||||
|
36
README.md
@ -1,5 +1,5 @@
|
||||
# Iced
|
||||
[](https://travis-ci.org/hecrj/iced)
|
||||
[](https://github.com/hecrj/iced/actions)
|
||||
[][documentation]
|
||||
[](https://crates.io/crates/iced)
|
||||
[](https://github.com/hecrj/iced/blob/master/LICENSE)
|
||||
@ -55,7 +55,7 @@ __Iced is currently experimental software.__ [Take a look at the roadmap],
|
||||
Add `iced` as a dependency in your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
iced = "0.1.0-beta"
|
||||
iced = "0.3"
|
||||
```
|
||||
|
||||
__Iced moves fast and the `master` branch can contain breaking changes!__ If
|
||||
@ -168,26 +168,19 @@ Browse the [documentation] and the [examples] to learn more!
|
||||
Iced was originally born as an attempt at bringing the simplicity of [Elm] and
|
||||
[The Elm Architecture] into [Coffee], a 2D game engine I am working on.
|
||||
|
||||
The core of the library was implemented during May in [this pull request].
|
||||
The core of the library was implemented during May 2019 in [this pull request].
|
||||
[The first alpha version] was eventually released as
|
||||
[a renderer-agnostic GUI library]. The library did not provide a renderer and
|
||||
implemented the current [tour example] on top of [`ggez`], a game library.
|
||||
|
||||
Since then, the focus has shifted towards providing a batteries-included,
|
||||
end-user-oriented GUI library, while keeping [the ecosystem] modular.
|
||||
end-user-oriented GUI library, while keeping [the ecosystem] modular:
|
||||
|
||||
Currently, Iced is a cross-platform GUI library built on top of smaller crates:
|
||||
|
||||
- [`iced_core`], a bunch of basic types that can be reused in different runtimes.
|
||||
- [`iced_native`], a renderer-agnostic native runtime implementing widget
|
||||
logic and a layout engine inspired by [`druid`].
|
||||
- [`iced_web`], an experimental web runtime that targets the DOM thanks to
|
||||
[`dodrio`].
|
||||
- [`iced_wgpu`], a renderer leveraging [`wgpu`], [`wgpu_glyph`], and
|
||||
[`font-kit`].
|
||||
- [`iced_winit`], a windowing shell on top of [`winit`].
|
||||
|
||||
[](https://github.com/hecrj/iced/blob/master/ECOSYSTEM.md)
|
||||
<p align="center">
|
||||
<a href="https://github.com/hecrj/iced/blob/master/ECOSYSTEM.md">
|
||||
<img alt="Iced Ecosystem" src="docs/graphs/ecosystem.png" width="80%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[this pull request]: https://github.com/hecrj/coffee/pull/35
|
||||
[The first alpha version]: https://github.com/hecrj/iced/tree/0.1.0-alpha
|
||||
@ -195,15 +188,6 @@ Currently, Iced is a cross-platform GUI library built on top of smaller crates:
|
||||
[tour example]: https://github.com/hecrj/iced/blob/master/examples/README.md#tour
|
||||
[`ggez`]: https://github.com/ggez/ggez
|
||||
[the ecosystem]: https://github.com/hecrj/iced/blob/master/ECOSYSTEM.md
|
||||
[`iced_core`]: https://github.com/hecrj/iced/tree/master/core
|
||||
[`iced_native`]: https://github.com/hecrj/iced/tree/master/native
|
||||
[`iced_web`]: https://github.com/hecrj/iced/tree/master/web
|
||||
[`iced_wgpu`]: https://github.com/hecrj/iced/tree/master/wgpu
|
||||
[`iced_winit`]: https://github.com/hecrj/iced/tree/master/winit
|
||||
[`druid`]: https://github.com/xi-editor/druid
|
||||
[`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph
|
||||
[`font-kit`]: https://github.com/servo/font-kit
|
||||
[`winit`]: https://github.com/rust-windowing/winit
|
||||
|
||||
## Contributing / Feedback
|
||||
Contributions are greatly appreciated! If you want to contribute, please
|
||||
@ -217,7 +201,7 @@ the [Rust Community Discord]. I go by `lone_scientist#9554` there.
|
||||
## Sponsors
|
||||
The development of Iced is sponsored by the [Cryptowatch] team at [Kraken.com]
|
||||
|
||||
[documentation]: https://docs.rs/iced
|
||||
[documentation]: https://docs.rs/iced/
|
||||
[examples]: https://github.com/hecrj/iced/tree/master/examples
|
||||
[Coffee]: https://github.com/hecrj/coffee
|
||||
[Elm]: https://elm-lang.org/
|
||||
|
45
ROADMAP.md
@ -6,21 +6,29 @@ Before diving into the roadmap, check out [the ecosystem overview] to get an ide
|
||||
[the ecosystem overview]: ECOSYSTEM.md
|
||||
|
||||
## Next steps
|
||||
Most of the work related to these features needs to happen in the `iced_native` path of the ecosystem, as the web already supports many of them.
|
||||
Most of the work related to these features needs to happen in the __native__ path of the ecosystem, as the web already supports many of them.
|
||||
|
||||
Once a step is completed, it is collapsed and added to this list:
|
||||
|
||||
* [x] Scrollables / Clippables ([#24])
|
||||
* [x] Text input widget ([#25])
|
||||
* [x] TodoMVC example ([#26])
|
||||
* [x] Async actions ([#27])
|
||||
* [x] Async actions ([#28])
|
||||
* [x] Custom layout engine ([#52])
|
||||
* [x] Event subscriptions ([#122])
|
||||
* [x] Custom styling ([#146])
|
||||
* [x] Canvas for 2D graphics ([#193])
|
||||
* [x] Basic overlay support ([#444])
|
||||
|
||||
[#24]: https://github.com/hecrj/iced/issues/24
|
||||
[#25]: https://github.com/hecrj/iced/issues/25
|
||||
[#26]: https://github.com/hecrj/iced/issues/26
|
||||
[#28]: https://github.com/hecrj/iced/issues/28
|
||||
[#52]: https://github.com/hecrj/iced/pull/52
|
||||
[#122]: https://github.com/hecrj/iced/pull/122
|
||||
[#146]: https://github.com/hecrj/iced/pull/146
|
||||
[#193]: https://github.com/hecrj/iced/pull/193
|
||||
[#444]: https://github.com/hecrj/iced/pull/444
|
||||
|
||||
### Multi-window support ([#27])
|
||||
Open and control multiple windows at runtime.
|
||||
@ -31,26 +39,6 @@ This approach should also allow us to perform custom optimizations for this part
|
||||
|
||||
[#27]: https://github.com/hecrj/iced/issues/27
|
||||
|
||||
### Event subscriptions ([#29])
|
||||
Besides performing async actions on demand, most applications also need to listen to events passively. An example of this could be a WebSocket connection, where messages can come in at any time.
|
||||
|
||||
The idea here is to also follow [Elm]'s footsteps. We can add a method `subscriptions(&self) -> Subscription<Message>` to `Application` and keep the subscriptions alive in the runtime.
|
||||
|
||||
The challenge here is designing the public API of subscriptions. Ideally, users should be able to create their own subscriptions and the GUI runtime should keep them alive by performing _subscription diffing_ (i.e. detecting when a subscription is added, changed, or removed). For this, we can take a look at [Elm] for inspiration.
|
||||
|
||||
[#29]: https://github.com/hecrj/iced/issues/29
|
||||
|
||||
### Layers ([#30])
|
||||
Currently, Iced assumes widgets cannot be laid out on top of each other. We should implement support for multiple layers of widgets.
|
||||
|
||||
This is a necessary feature to implement many kinds of interactables, like dropdown menus, select fields, etc.
|
||||
|
||||
`iced_native` will need to group widgets to perform layouting and process some events first for widgets positioned on top.
|
||||
|
||||
`iced_wgpu` will also need to process the scene graph and sort draw calls based on the different layers.
|
||||
|
||||
[#30]: https://github.com/hecrj/iced/issues/30
|
||||
|
||||
### Animations ([#31])
|
||||
Allow widgets to request a redraw at a specific time.
|
||||
|
||||
@ -60,8 +48,8 @@ This is a necessary feature to render loading spinners, a blinking text cursor,
|
||||
|
||||
[#31]: https://github.com/hecrj/iced/issues/31
|
||||
|
||||
### Canvas widget ([#32])
|
||||
A widget to draw freely in 2D or 3D. It could be used to draw charts, implement a Paint clone, a CAD application, etc.
|
||||
### Canvas widget for 3D graphics ([#32])
|
||||
A widget to draw freely in 3D. It could be used to draw charts, implement a Paint clone, a CAD application, etc.
|
||||
|
||||
As a first approach, we could expose the underlying renderer directly here, and couple this widget with it ([`wgpu`] for now). Once [`wgpu`] gets WebGL or WebGPU support, this widget will be able to run on the web too. The renderer primitive could be a simple texture that the widget draws to.
|
||||
|
||||
@ -114,15 +102,6 @@ Once the state lifetime of widgets is removed, we could keep them alive between
|
||||
|
||||
This is a big undertaking and introduces a new set of problems. We should research and consider the implications of this approach in detail before going for it.
|
||||
|
||||
### Improve style definitions
|
||||
As of now, each widget defines its own styling options with methods, following the builder pattern.
|
||||
|
||||
A unified way of defining and reusing styles would be great. I think we must avoid replicating CSS, we should try to stay as type-safe, explicit, and intuitive as possible.
|
||||
|
||||
I think many different ideas in [`elm-ui`] could serve as an inspiration.
|
||||
|
||||
[`elm-ui`]: https://www.youtube.com/watch?v=Ie-gqwSHQr0
|
||||
|
||||
### Try a different font rasterizer
|
||||
[`wgpu_glyph`] depends indirectly on [`rusttype`]. We may be able to gain performance by using a different font rasterizer. [`fontdue`], for instance, has reported noticeable speedups.
|
||||
|
||||
|
@ -1,14 +0,0 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0] - 2019-11-25
|
||||
### Added
|
||||
- First release! :tada:
|
||||
|
||||
[Unreleased]: https://github.com/hecrj/iced/compare/core-0.1.0...HEAD
|
||||
[0.1.0]: https://github.com/hecrj/iced/releases/tag/core-0.1.0
|
@ -1,15 +1,14 @@
|
||||
[package]
|
||||
name = "iced_core"
|
||||
version = "0.1.0"
|
||||
version = "0.4.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "The essential concepts of Iced"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/hecrj/iced"
|
||||
|
||||
[features]
|
||||
# Exposes a future-based `Command` type
|
||||
command = ["futures"]
|
||||
|
||||
[dependencies]
|
||||
futures = { version = "0.3", optional = true }
|
||||
|
||||
[dependencies.palette]
|
||||
version = "0.5.0"
|
||||
optional = true
|
||||
|
@ -8,7 +8,9 @@
|
||||
|
||||
This crate is meant to be a starting point for an Iced runtime.
|
||||
|
||||

|
||||
<p align="center">
|
||||
<img alt="The foundations" src="../docs/graphs/foundations.png" width="50%">
|
||||
</p>
|
||||
|
||||
[documentation]: https://docs.rs/iced_core
|
||||
|
||||
@ -16,7 +18,7 @@ This crate is meant to be a starting point for an Iced runtime.
|
||||
Add `iced_core` as a dependency in your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
iced_core = "0.1.0"
|
||||
iced_core = "0.4"
|
||||
```
|
||||
|
||||
__Iced moves fast and the `master` branch can contain breaking changes!__ If
|
||||
|
@ -7,3 +7,15 @@ pub enum Background {
|
||||
Color(Color),
|
||||
// TODO: Add gradient and image variants
|
||||
}
|
||||
|
||||
impl From<Color> for Background {
|
||||
fn from(color: Color) -> Self {
|
||||
Background::Color(color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Option<Background> {
|
||||
fn from(color: Color) -> Self {
|
||||
Some(Background::from(color))
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,16 @@
|
||||
#[cfg(feature = "palette")]
|
||||
use palette::rgb::{Srgb, Srgba};
|
||||
|
||||
/// A color in the sRGB color space.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct Color {
|
||||
/// Red component, 0.0 - 1.0
|
||||
pub r: f32,
|
||||
/// Green component, 0.0 - 1.0
|
||||
pub g: f32,
|
||||
/// Blue component, 0.0 - 1.0
|
||||
pub b: f32,
|
||||
/// Transparency, 0.0 - 1.0
|
||||
pub a: f32,
|
||||
}
|
||||
|
||||
@ -25,9 +31,65 @@ impl Color {
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
/// Converts the [`Color`] into its linear values.
|
||||
/// A color with no opacity.
|
||||
pub const TRANSPARENT: Color = Color {
|
||||
r: 0.0,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 0.0,
|
||||
};
|
||||
|
||||
/// Creates a new [`Color`].
|
||||
///
|
||||
/// [`Color`]: struct.Color.html
|
||||
/// In debug mode, it will panic if the values are not in the correct
|
||||
/// range: 0.0 - 1.0
|
||||
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Color {
|
||||
debug_assert!(
|
||||
(0.0..=1.0).contains(&r),
|
||||
"Red component must be on [0, 1]"
|
||||
);
|
||||
debug_assert!(
|
||||
(0.0..=1.0).contains(&g),
|
||||
"Green component must be on [0, 1]"
|
||||
);
|
||||
debug_assert!(
|
||||
(0.0..=1.0).contains(&b),
|
||||
"Blue component must be on [0, 1]"
|
||||
);
|
||||
debug_assert!(
|
||||
(0.0..=1.0).contains(&a),
|
||||
"Alpha component must be on [0, 1]"
|
||||
);
|
||||
|
||||
Color { r, g, b, a }
|
||||
}
|
||||
|
||||
/// Creates a [`Color`] from its RGB components.
|
||||
pub const fn from_rgb(r: f32, g: f32, b: f32) -> Color {
|
||||
Color::from_rgba(r, g, b, 1.0f32)
|
||||
}
|
||||
|
||||
/// Creates a [`Color`] from its RGBA components.
|
||||
pub const fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
|
||||
Color { r, g, b, a }
|
||||
}
|
||||
|
||||
/// Creates a [`Color`] from its RGB8 components.
|
||||
pub fn from_rgb8(r: u8, g: u8, b: u8) -> Color {
|
||||
Color::from_rgba8(r, g, b, 1.0)
|
||||
}
|
||||
|
||||
/// Creates a [`Color`] from its RGB8 components and an alpha value.
|
||||
pub fn from_rgba8(r: u8, g: u8, b: u8, a: f32) -> Color {
|
||||
Color {
|
||||
r: f32::from(r) / 255.0,
|
||||
g: f32::from(g) / 255.0,
|
||||
b: f32::from(b) / 255.0,
|
||||
a,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the [`Color`] into its linear values.
|
||||
pub fn into_linear(self) -> [f32; 4] {
|
||||
// As described in:
|
||||
// https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
|
||||
@ -46,16 +108,101 @@ impl Color {
|
||||
self.a,
|
||||
]
|
||||
}
|
||||
|
||||
/// Inverts the [`Color`] in-place.
|
||||
pub fn invert(&mut self) {
|
||||
self.r = 1.0f32 - self.r;
|
||||
self.b = 1.0f32 - self.g;
|
||||
self.g = 1.0f32 - self.b;
|
||||
}
|
||||
|
||||
/// Returns the inverted [`Color`].
|
||||
pub fn inverse(self) -> Color {
|
||||
Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[f32; 3]> for Color {
|
||||
fn from([r, g, b]: [f32; 3]) -> Self {
|
||||
Color { r, g, b, a: 1.0 }
|
||||
Color::new(r, g, b, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[f32; 4]> for Color {
|
||||
fn from([r, g, b, a]: [f32; 4]) -> Self {
|
||||
Color { r, g, b, a }
|
||||
Color::new(r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "palette")]
|
||||
/// Converts from palette's `Srgba` type to a [`Color`].
|
||||
impl From<Srgba> for Color {
|
||||
fn from(srgba: Srgba) -> Self {
|
||||
Color::new(srgba.red, srgba.green, srgba.blue, srgba.alpha)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "palette")]
|
||||
/// Converts from [`Color`] to palette's `Srgba` type.
|
||||
impl From<Color> for Srgba {
|
||||
fn from(c: Color) -> Self {
|
||||
Srgba::new(c.r, c.g, c.b, c.a)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "palette")]
|
||||
/// Converts from palette's `Srgb` type to a [`Color`].
|
||||
impl From<Srgb> for Color {
|
||||
fn from(srgb: Srgb) -> Self {
|
||||
Color::new(srgb.red, srgb.green, srgb.blue, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "palette")]
|
||||
/// Converts from [`Color`] to palette's `Srgb` type.
|
||||
impl From<Color> for Srgb {
|
||||
fn from(c: Color) -> Self {
|
||||
Srgb::new(c.r, c.g, c.b)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "palette")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use palette::Blend;
|
||||
|
||||
#[test]
|
||||
fn srgba_traits() {
|
||||
let c = Color::from_rgb(0.5, 0.4, 0.3);
|
||||
// Round-trip conversion to the palette:Srgba type
|
||||
let s: Srgba = c.into();
|
||||
let r: Color = s.into();
|
||||
assert_eq!(c, r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_manipulation() {
|
||||
let c1 = Color::from_rgb(0.5, 0.4, 0.3);
|
||||
let c2 = Color::from_rgb(0.2, 0.5, 0.3);
|
||||
|
||||
// Convert to linear color for manipulation
|
||||
let l1 = Srgba::from(c1).into_linear();
|
||||
let l2 = Srgba::from(c2).into_linear();
|
||||
|
||||
// Take the lighter of each of the RGB components
|
||||
let lighter = l1.lighten(l2);
|
||||
|
||||
// Convert back to our Color
|
||||
let r: Color = Srgba::from_linear(lighter).into();
|
||||
assert_eq!(
|
||||
r,
|
||||
Color {
|
||||
r: 0.5,
|
||||
g: 0.5,
|
||||
b: 0.3,
|
||||
a: 1.0
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,72 +0,0 @@
|
||||
use futures::future::{BoxFuture, Future, FutureExt};
|
||||
|
||||
/// A collection of async operations.
|
||||
///
|
||||
/// You should be able to turn a future easily into a [`Command`], either by
|
||||
/// using the `From` trait or [`Command::perform`].
|
||||
///
|
||||
/// [`Command`]: struct.Command.html
|
||||
pub struct Command<T> {
|
||||
futures: Vec<BoxFuture<'static, T>>,
|
||||
}
|
||||
|
||||
impl<T> Command<T> {
|
||||
/// Creates an empty [`Command`].
|
||||
///
|
||||
/// In other words, a [`Command`] that does nothing.
|
||||
///
|
||||
/// [`Command`]: struct.Command.html
|
||||
pub fn none() -> Self {
|
||||
Self {
|
||||
futures: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`Command`] that performs the action of the given future.
|
||||
///
|
||||
/// [`Command`]: struct.Command.html
|
||||
pub fn perform<A>(
|
||||
future: impl Future<Output = T> + 'static + Send,
|
||||
f: impl Fn(T) -> A + 'static + Send,
|
||||
) -> Command<A> {
|
||||
Command {
|
||||
futures: vec![future.map(f).boxed()],
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`Command`] that performs the actions of all the givens
|
||||
/// futures.
|
||||
///
|
||||
/// Once this command is run, all the futures will be exectued at once.
|
||||
///
|
||||
/// [`Command`]: struct.Command.html
|
||||
pub fn batch(commands: impl Iterator<Item = Command<T>>) -> Self {
|
||||
Self {
|
||||
futures: commands.flat_map(|command| command.futures).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a [`Command`] into its underlying list of futures.
|
||||
///
|
||||
/// [`Command`]: struct.Command.html
|
||||
pub fn futures(self) -> Vec<BoxFuture<'static, T>> {
|
||||
self.futures
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, A> From<A> for Command<T>
|
||||
where
|
||||
A: Future<Output = T> + 'static + Send,
|
||||
{
|
||||
fn from(future: A) -> Self {
|
||||
Self {
|
||||
futures: vec![future.boxed()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Debug for Command<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Command").finish()
|
||||
}
|
||||
}
|
@ -16,3 +16,9 @@ pub enum Font {
|
||||
bytes: &'static [u8],
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for Font {
|
||||
fn default() -> Font {
|
||||
Font::Default
|
||||
}
|
||||
}
|
||||
|
8
core/src/keyboard.rs
Normal file
@ -0,0 +1,8 @@
|
||||
//! Reuse basic keyboard types.
|
||||
mod event;
|
||||
mod key_code;
|
||||
mod modifiers;
|
||||
|
||||
pub use event::Event;
|
||||
pub use key_code::KeyCode;
|
||||
pub use modifiers::Modifiers;
|
@ -1,23 +1,34 @@
|
||||
use super::KeyCode;
|
||||
use crate::input::ButtonState;
|
||||
use super::{KeyCode, Modifiers};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
/// A keyboard event.
|
||||
///
|
||||
/// _**Note:** This type is largely incomplete! If you need to track
|
||||
/// additional events, feel free to [open an issue] and share your use case!_
|
||||
///
|
||||
/// [open an issue]: https://github.com/hecrj/iced/issues
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Event {
|
||||
/// A keyboard key was pressed or released.
|
||||
Input {
|
||||
/// The state of the key
|
||||
state: ButtonState,
|
||||
|
||||
/// A keyboard key was pressed.
|
||||
KeyPressed {
|
||||
/// The key identifier
|
||||
key_code: KeyCode,
|
||||
|
||||
/// The state of the modifier keys
|
||||
modifiers: Modifiers,
|
||||
},
|
||||
|
||||
/// A keyboard key was released.
|
||||
KeyReleased {
|
||||
/// The key identifier
|
||||
key_code: KeyCode,
|
||||
|
||||
/// The state of the modifier keys
|
||||
modifiers: Modifiers,
|
||||
},
|
||||
|
||||
/// A unicode character was received.
|
||||
CharacterReceived(char),
|
||||
|
||||
/// The keyboard modifiers have changed.
|
||||
ModifiersChanged(Modifiers),
|
||||
}
|
@ -55,7 +55,7 @@ pub enum KeyCode {
|
||||
Y,
|
||||
Z,
|
||||
|
||||
/// The Escape key, next to F1
|
||||
/// The Escape key, next to F1.
|
||||
Escape,
|
||||
|
||||
F1,
|
||||
@ -83,14 +83,14 @@ pub enum KeyCode {
|
||||
F23,
|
||||
F24,
|
||||
|
||||
/// Print Screen/SysRq
|
||||
/// Print Screen/SysRq.
|
||||
Snapshot,
|
||||
/// Scroll Lock
|
||||
/// Scroll Lock.
|
||||
Scroll,
|
||||
/// Pause/Break key, next to Scroll lock
|
||||
/// Pause/Break key, next to Scroll lock.
|
||||
Pause,
|
||||
|
||||
/// `Insert`, next to Backspace
|
||||
/// `Insert`, next to Backspace.
|
||||
Insert,
|
||||
Home,
|
||||
Delete,
|
||||
@ -103,11 +103,14 @@ pub enum KeyCode {
|
||||
Right,
|
||||
Down,
|
||||
|
||||
/// The Backspace key, right over Enter.
|
||||
Backspace,
|
||||
/// The Enter key.
|
||||
Enter,
|
||||
/// The space bar.
|
||||
Space,
|
||||
|
||||
/// The "Compose" key on Linux
|
||||
/// The "Compose" key on Linux.
|
||||
Compose,
|
||||
|
||||
Caret,
|
||||
@ -123,12 +126,20 @@ pub enum KeyCode {
|
||||
Numpad7,
|
||||
Numpad8,
|
||||
Numpad9,
|
||||
NumpadAdd,
|
||||
NumpadDivide,
|
||||
NumpadDecimal,
|
||||
NumpadComma,
|
||||
NumpadEnter,
|
||||
NumpadEquals,
|
||||
NumpadMultiply,
|
||||
NumpadSubtract,
|
||||
|
||||
AbntC1,
|
||||
AbntC2,
|
||||
Add,
|
||||
Apostrophe,
|
||||
Apps,
|
||||
Asterisk,
|
||||
At,
|
||||
Ax,
|
||||
Backslash,
|
||||
@ -137,8 +148,6 @@ pub enum KeyCode {
|
||||
Colon,
|
||||
Comma,
|
||||
Convert,
|
||||
Decimal,
|
||||
Divide,
|
||||
Equals,
|
||||
Grave,
|
||||
Kana,
|
||||
@ -152,19 +161,16 @@ pub enum KeyCode {
|
||||
MediaSelect,
|
||||
MediaStop,
|
||||
Minus,
|
||||
Multiply,
|
||||
Mute,
|
||||
MyComputer,
|
||||
NavigateForward, // also called "Prior"
|
||||
NavigateBackward, // also called "Next"
|
||||
NavigateForward, // also called "Next"
|
||||
NavigateBackward, // also called "Prior"
|
||||
NextTrack,
|
||||
NoConvert,
|
||||
NumpadComma,
|
||||
NumpadEnter,
|
||||
NumpadEquals,
|
||||
OEM102,
|
||||
Period,
|
||||
PlayPause,
|
||||
Plus,
|
||||
Power,
|
||||
PrevTrack,
|
||||
RAlt,
|
||||
@ -176,7 +182,6 @@ pub enum KeyCode {
|
||||
Slash,
|
||||
Sleep,
|
||||
Stop,
|
||||
Subtract,
|
||||
Sysrq,
|
||||
Tab,
|
||||
Underline,
|
45
core/src/keyboard/modifiers.rs
Normal file
@ -0,0 +1,45 @@
|
||||
/// The current state of the keyboard modifiers.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Modifiers {
|
||||
/// Whether a shift key is pressed
|
||||
pub shift: bool,
|
||||
|
||||
/// Whether a control key is pressed
|
||||
pub control: bool,
|
||||
|
||||
/// Whether an alt key is pressed
|
||||
pub alt: bool,
|
||||
|
||||
/// Whether a logo key is pressed (e.g. windows key, command key...)
|
||||
pub logo: bool,
|
||||
}
|
||||
|
||||
impl Modifiers {
|
||||
/// Returns true if a "command key" is pressed in the [`Modifiers`].
|
||||
///
|
||||
/// The "command key" is the main modifier key used to issue commands in the
|
||||
/// current platform. Specifically:
|
||||
///
|
||||
/// - It is the `logo` or command key (⌘) on macOS
|
||||
/// - It is the `control` key on other platforms
|
||||
pub fn is_command_pressed(self) -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
let is_pressed = self.logo;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let is_pressed = self.control;
|
||||
|
||||
is_pressed
|
||||
}
|
||||
|
||||
/// Returns true if the current [`Modifiers`] have at least the same
|
||||
/// keys pressed as the provided ones, and false otherwise.
|
||||
pub fn matches(&self, modifiers: Self) -> bool {
|
||||
let shift = !modifiers.shift || self.shift;
|
||||
let control = !modifiers.control || self.control;
|
||||
let alt = !modifiers.alt || self.alt;
|
||||
let logo = !modifiers.logo || self.logo;
|
||||
|
||||
shift && control && alt && logo
|
||||
}
|
||||
}
|
@ -4,6 +4,15 @@ pub enum Length {
|
||||
/// Fill all the remaining space
|
||||
Fill,
|
||||
|
||||
/// Fill a portion of the remaining space relative to other elements.
|
||||
///
|
||||
/// Let's say we have two elements: one with `FillPortion(2)` and one with
|
||||
/// `FillPortion(3)`. The first will get 2 portions of the available space,
|
||||
/// while the second one would get 3.
|
||||
///
|
||||
/// `Length::Fill` is equivalent to `Length::FillPortion(1)`.
|
||||
FillPortion(u16),
|
||||
|
||||
/// Fill the least amount of space
|
||||
Shrink,
|
||||
|
||||
@ -17,13 +26,18 @@ impl Length {
|
||||
/// The _fill factor_ is a relative unit describing how much of the
|
||||
/// remaining space should be filled when compared to other elements. It
|
||||
/// is only meant to be used by layout engines.
|
||||
///
|
||||
/// [`Length`]: enum.Length.html
|
||||
pub fn fill_factor(&self) -> u16 {
|
||||
match self {
|
||||
Length::Fill => 1,
|
||||
Length::FillPortion(factor) => *factor,
|
||||
Length::Shrink => 0,
|
||||
Length::Units(_) => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Length {
|
||||
fn from(units: u16) -> Self {
|
||||
Length::Units(units)
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,31 @@
|
||||
//! The core library of [Iced].
|
||||
//!
|
||||
//! 
|
||||
//!
|
||||
//! This library holds basic types that can be reused and re-exported in
|
||||
//! different runtime implementations. For instance, both [`iced_native`] and
|
||||
//! [`iced_web`] are built on top of `iced_core`.
|
||||
//!
|
||||
//! 
|
||||
//!
|
||||
//! [Iced]: https://github.com/hecrj/iced
|
||||
//! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
|
||||
//! [`iced_web`]: https://github.com/hecrj/iced/tree/master/web
|
||||
#![deny(missing_docs)]
|
||||
#![deny(missing_debug_implementations)]
|
||||
#![deny(unused_results)]
|
||||
#![deny(unsafe_code)]
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![forbid(unsafe_code)]
|
||||
#![forbid(rust_2018_idioms)]
|
||||
pub mod keyboard;
|
||||
pub mod mouse;
|
||||
|
||||
mod align;
|
||||
mod background;
|
||||
mod color;
|
||||
mod font;
|
||||
mod length;
|
||||
mod padding;
|
||||
mod point;
|
||||
mod rectangle;
|
||||
mod size;
|
||||
mod vector;
|
||||
|
||||
pub use align::{Align, HorizontalAlignment, VerticalAlignment};
|
||||
@ -29,12 +33,8 @@ pub use background::Background;
|
||||
pub use color::Color;
|
||||
pub use font::Font;
|
||||
pub use length::Length;
|
||||
pub use padding::Padding;
|
||||
pub use point::Point;
|
||||
pub use rectangle::Rectangle;
|
||||
pub use size::Size;
|
||||
pub use vector::Vector;
|
||||
|
||||
#[cfg(feature = "command")]
|
||||
mod command;
|
||||
|
||||
#[cfg(feature = "command")]
|
||||
pub use command::Command;
|
||||
|
@ -1,6 +1,8 @@
|
||||
//! Build mouse events.
|
||||
//! Reuse basic mouse types.
|
||||
mod button;
|
||||
mod event;
|
||||
mod interaction;
|
||||
|
||||
pub use button::Button;
|
||||
pub use event::{Event, ScrollDelta};
|
||||
pub use interaction::Interaction;
|
@ -1,5 +1,6 @@
|
||||
use crate::Point;
|
||||
|
||||
use super::Button;
|
||||
use crate::input::ButtonState;
|
||||
|
||||
/// A mouse event.
|
||||
///
|
||||
@ -17,21 +18,15 @@ pub enum Event {
|
||||
|
||||
/// The mouse cursor was moved
|
||||
CursorMoved {
|
||||
/// The X coordinate of the mouse position
|
||||
x: f32,
|
||||
|
||||
/// The Y coordinate of the mouse position
|
||||
y: f32,
|
||||
/// The new position of the mouse cursor
|
||||
position: Point,
|
||||
},
|
||||
|
||||
/// A mouse button was pressed or released.
|
||||
Input {
|
||||
/// The state of the button
|
||||
state: ButtonState,
|
||||
/// A mouse button was pressed.
|
||||
ButtonPressed(Button),
|
||||
|
||||
/// The button identifier
|
||||
button: Button,
|
||||
},
|
||||
/// A mouse button was released.
|
||||
ButtonReleased(Button),
|
||||
|
||||
/// The mouse wheel was scrolled.
|
||||
WheelScrolled {
|
20
core/src/mouse/interaction.rs
Normal file
@ -0,0 +1,20 @@
|
||||
/// The interaction of a mouse cursor.
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, PartialOrd, Ord)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Interaction {
|
||||
Idle,
|
||||
Pointer,
|
||||
Grab,
|
||||
Text,
|
||||
Crosshair,
|
||||
Working,
|
||||
Grabbing,
|
||||
ResizingHorizontally,
|
||||
ResizingVertically,
|
||||
}
|
||||
|
||||
impl Default for Interaction {
|
||||
fn default() -> Interaction {
|
||||
Interaction::Idle
|
||||
}
|
||||
}
|
107
core/src/padding.rs
Normal file
@ -0,0 +1,107 @@
|
||||
/// An amount of space to pad for each side of a box
|
||||
///
|
||||
/// You can leverage the `From` trait to build [`Padding`] conveniently:
|
||||
///
|
||||
/// ```
|
||||
/// # use iced_core::Padding;
|
||||
/// #
|
||||
/// let padding = Padding::from(20); // 20px on all sides
|
||||
/// let padding = Padding::from([10, 20]); // top/bottom, left/right
|
||||
/// let padding = Padding::from([5, 10, 15, 20]); // top, right, bottom, left
|
||||
/// ```
|
||||
///
|
||||
/// Normally, the `padding` method of a widget will ask for an `Into<Padding>`,
|
||||
/// so you can easily write:
|
||||
///
|
||||
/// ```
|
||||
/// # use iced_core::Padding;
|
||||
/// #
|
||||
/// # struct Widget;
|
||||
/// #
|
||||
/// impl Widget {
|
||||
/// # pub fn new() -> Self { Self }
|
||||
/// #
|
||||
/// pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
|
||||
/// // ...
|
||||
/// self
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let widget = Widget::new().padding(20); // 20px on all sides
|
||||
/// let widget = Widget::new().padding([10, 20]); // top/bottom, left/right
|
||||
/// let widget = Widget::new().padding([5, 10, 15, 20]); // top, right, bottom, left
|
||||
/// ```
|
||||
#[derive(Debug, Hash, Copy, Clone)]
|
||||
pub struct Padding {
|
||||
/// Top padding
|
||||
pub top: u16,
|
||||
/// Right padding
|
||||
pub right: u16,
|
||||
/// Bottom padding
|
||||
pub bottom: u16,
|
||||
/// Left padding
|
||||
pub left: u16,
|
||||
}
|
||||
|
||||
impl Padding {
|
||||
/// Padding of zero
|
||||
pub const ZERO: Padding = Padding {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
};
|
||||
|
||||
/// Create a Padding that is equal on all sides
|
||||
pub const fn new(padding: u16) -> Padding {
|
||||
Padding {
|
||||
top: padding,
|
||||
right: padding,
|
||||
bottom: padding,
|
||||
left: padding,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the total amount of vertical [`Padding`].
|
||||
pub fn vertical(self) -> u16 {
|
||||
self.top + self.bottom
|
||||
}
|
||||
|
||||
/// Returns the total amount of horizontal [`Padding`].
|
||||
pub fn horizontal(self) -> u16 {
|
||||
self.left + self.right
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<u16> for Padding {
|
||||
fn from(p: u16) -> Self {
|
||||
Padding {
|
||||
top: p,
|
||||
right: p,
|
||||
bottom: p,
|
||||
left: p,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<[u16; 2]> for Padding {
|
||||
fn from(p: [u16; 2]) -> Self {
|
||||
Padding {
|
||||
top: p[0],
|
||||
right: p[1],
|
||||
bottom: p[0],
|
||||
left: p[1],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<[u16; 4]> for Padding {
|
||||
fn from(p: [u16; 4]) -> Self {
|
||||
Padding {
|
||||
top: p[0],
|
||||
right: p[1],
|
||||
bottom: p[2],
|
||||
left: p[3],
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
use crate::Vector;
|
||||
|
||||
/// A 2D point.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct Point {
|
||||
/// The X coordinate.
|
||||
pub x: f32,
|
||||
@ -11,12 +11,39 @@ pub struct Point {
|
||||
}
|
||||
|
||||
impl Point {
|
||||
/// The origin (i.e. a [`Point`] at (0, 0)).
|
||||
pub const ORIGIN: Point = Point::new(0.0, 0.0);
|
||||
|
||||
/// Creates a new [`Point`] with the given coordinates.
|
||||
///
|
||||
/// [`Point`]: struct.Point.html
|
||||
pub fn new(x: f32, y: f32) -> Self {
|
||||
pub const fn new(x: f32, y: f32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
/// Computes the distance to another [`Point`].
|
||||
pub fn distance(&self, to: Point) -> f32 {
|
||||
let a = self.x - to.x;
|
||||
let b = self.y - to.y;
|
||||
|
||||
a.hypot(b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[f32; 2]> for Point {
|
||||
fn from([x, y]: [f32; 2]) -> Self {
|
||||
Point { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u16; 2]> for Point {
|
||||
fn from([x, y]: [u16; 2]) -> Self {
|
||||
Point::new(x.into(), y.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Point> for [f32; 2] {
|
||||
fn from(point: Point) -> [f32; 2] {
|
||||
[point.x, point.y]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add<Vector> for Point {
|
||||
@ -29,3 +56,22 @@ impl std::ops::Add<Vector> for Point {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub<Vector> for Point {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, vector: Vector) -> Self {
|
||||
Self {
|
||||
x: self.x - vector.x,
|
||||
y: self.y - vector.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub<Point> for Point {
|
||||
type Output = Vector;
|
||||
|
||||
fn sub(self, point: Point) -> Vector {
|
||||
Vector::new(self.x - point.x, self.y - point.y)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::Point;
|
||||
use crate::{Point, Size, Vector};
|
||||
|
||||
/// A rectangle.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
@ -17,27 +17,135 @@ pub struct Rectangle<T = f32> {
|
||||
}
|
||||
|
||||
impl Rectangle<f32> {
|
||||
/// Creates a new [`Rectangle`] with its top-left corner in the given
|
||||
/// [`Point`] and with the provided [`Size`].
|
||||
pub fn new(top_left: Point, size: Size) -> Self {
|
||||
Self {
|
||||
x: top_left.x,
|
||||
y: top_left.y,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`Rectangle`] with its top-left corner at the origin
|
||||
/// and with the provided [`Size`].
|
||||
pub fn with_size(size: Size) -> Self {
|
||||
Self {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`Point`] at the center of the [`Rectangle`].
|
||||
pub fn center(&self) -> Point {
|
||||
Point::new(self.center_x(), self.center_y())
|
||||
}
|
||||
|
||||
/// Returns the X coordinate of the [`Point`] at the center of the
|
||||
/// [`Rectangle`].
|
||||
pub fn center_x(&self) -> f32 {
|
||||
self.x + self.width / 2.0
|
||||
}
|
||||
|
||||
/// Returns the Y coordinate of the [`Point`] at the center of the
|
||||
/// [`Rectangle`].
|
||||
pub fn center_y(&self) -> f32 {
|
||||
self.y + self.height / 2.0
|
||||
}
|
||||
|
||||
/// Returns the position of the top left corner of the [`Rectangle`].
|
||||
pub fn position(&self) -> Point {
|
||||
Point::new(self.x, self.y)
|
||||
}
|
||||
|
||||
/// Returns the [`Size`] of the [`Rectangle`].
|
||||
pub fn size(&self) -> Size {
|
||||
Size::new(self.width, self.height)
|
||||
}
|
||||
|
||||
/// Returns true if the given [`Point`] is contained in the [`Rectangle`].
|
||||
///
|
||||
/// [`Point`]: struct.Point.html
|
||||
/// [`Rectangle`]: struct.Rectangle.html
|
||||
pub fn contains(&self, point: Point) -> bool {
|
||||
self.x <= point.x
|
||||
&& point.x <= self.x + self.width
|
||||
&& self.y <= point.y
|
||||
&& point.y <= self.y + self.height
|
||||
}
|
||||
|
||||
/// Computes the intersection with the given [`Rectangle`].
|
||||
pub fn intersection(
|
||||
&self,
|
||||
other: &Rectangle<f32>,
|
||||
) -> Option<Rectangle<f32>> {
|
||||
let x = self.x.max(other.x);
|
||||
let y = self.y.max(other.y);
|
||||
|
||||
let lower_right_x = (self.x + self.width).min(other.x + other.width);
|
||||
let lower_right_y = (self.y + self.height).min(other.y + other.height);
|
||||
|
||||
let width = lower_right_x - x;
|
||||
let height = lower_right_y - y;
|
||||
|
||||
if width > 0.0 && height > 0.0 {
|
||||
Some(Rectangle {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Snaps the [`Rectangle`] to __unsigned__ integer coordinates.
|
||||
pub fn snap(self) -> Rectangle<u32> {
|
||||
Rectangle {
|
||||
x: self.x as u32,
|
||||
y: self.y as u32,
|
||||
width: self.width as u32,
|
||||
height: self.height as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<f32> for Rectangle<u32> {
|
||||
impl std::ops::Mul<f32> for Rectangle<f32> {
|
||||
type Output = Self;
|
||||
|
||||
fn mul(self, scale: f32) -> Self {
|
||||
Self {
|
||||
x: (self.x as f32 * scale).round() as u32,
|
||||
y: (self.y as f32 * scale).round() as u32,
|
||||
width: (self.width as f32 * scale).round() as u32,
|
||||
height: (self.height as f32 * scale).round() as u32,
|
||||
x: self.x as f32 * scale,
|
||||
y: self.y as f32 * scale,
|
||||
width: self.width * scale,
|
||||
height: self.height * scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rectangle<u32>> for Rectangle<f32> {
|
||||
fn from(rectangle: Rectangle<u32>) -> Rectangle<f32> {
|
||||
Rectangle {
|
||||
x: rectangle.x as f32,
|
||||
y: rectangle.y as f32,
|
||||
width: rectangle.width as f32,
|
||||
height: rectangle.height as f32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Add<Vector<T>> for Rectangle<T>
|
||||
where
|
||||
T: std::ops::Add<Output = T>,
|
||||
{
|
||||
type Output = Rectangle<T>;
|
||||
|
||||
fn add(self, translation: Vector<T>) -> Self {
|
||||
Rectangle {
|
||||
x: self.x + translation.x,
|
||||
y: self.y + translation.y,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
70
core/src/size.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use crate::{Padding, Vector};
|
||||
use std::f32;
|
||||
|
||||
/// An amount of space in 2 dimensions.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Size<T = f32> {
|
||||
/// The width.
|
||||
pub width: T,
|
||||
/// The height.
|
||||
pub height: T,
|
||||
}
|
||||
|
||||
impl<T> Size<T> {
|
||||
/// Creates a new [`Size`] with the given width and height.
|
||||
pub const fn new(width: T, height: T) -> Self {
|
||||
Size { width, height }
|
||||
}
|
||||
}
|
||||
|
||||
impl Size {
|
||||
/// A [`Size`] with zero width and height.
|
||||
pub const ZERO: Size = Size::new(0., 0.);
|
||||
|
||||
/// A [`Size`] with a width and height of 1 unit.
|
||||
pub const UNIT: Size = Size::new(1., 1.);
|
||||
|
||||
/// A [`Size`] with infinite width and height.
|
||||
pub const INFINITY: Size = Size::new(f32::INFINITY, f32::INFINITY);
|
||||
|
||||
/// Increments the [`Size`] to account for the given padding.
|
||||
pub fn pad(&self, padding: Padding) -> Self {
|
||||
Size {
|
||||
width: self.width + padding.horizontal() as f32,
|
||||
height: self.height + padding.vertical() as f32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[f32; 2]> for Size {
|
||||
fn from([width, height]: [f32; 2]) -> Self {
|
||||
Size { width, height }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u16; 2]> for Size {
|
||||
fn from([width, height]: [u16; 2]) -> Self {
|
||||
Size::new(width.into(), height.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vector<f32>> for Size {
|
||||
fn from(vector: Vector<f32>) -> Self {
|
||||
Size {
|
||||
width: vector.x,
|
||||
height: vector.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Size> for [f32; 2] {
|
||||
fn from(size: Size) -> [f32; 2] {
|
||||
[size.width, size.height]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Size> for Vector<f32> {
|
||||
fn from(size: Size) -> Self {
|
||||
Vector::new(size.width, size.height)
|
||||
}
|
||||
}
|
@ -2,21 +2,15 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Vector<T = f32> {
|
||||
/// The X component of the [`Vector`]
|
||||
///
|
||||
/// [`Vector`]: struct.Vector.html
|
||||
pub x: T,
|
||||
|
||||
/// The Y component of the [`Vector`]
|
||||
///
|
||||
/// [`Vector`]: struct.Vector.html
|
||||
pub y: T,
|
||||
}
|
||||
|
||||
impl<T> Vector<T> {
|
||||
/// Creates a new [`Vector`] with the given components.
|
||||
///
|
||||
/// [`Vector`]: struct.Vector.html
|
||||
pub fn new(x: T, y: T) -> Self {
|
||||
pub const fn new(x: T, y: T) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
@ -31,3 +25,52 @@ where
|
||||
Self::new(self.x + b.x, self.y + b.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Sub for Vector<T>
|
||||
where
|
||||
T: std::ops::Sub<Output = T>,
|
||||
{
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, b: Self) -> Self {
|
||||
Self::new(self.x - b.x, self.y - b.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Mul<T> for Vector<T>
|
||||
where
|
||||
T: std::ops::Mul<Output = T> + Copy,
|
||||
{
|
||||
type Output = Self;
|
||||
|
||||
fn mul(self, scale: T) -> Self {
|
||||
Self::new(self.x * scale, self.y * scale)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for Vector<T>
|
||||
where
|
||||
T: Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
x: T::default(),
|
||||
y: T::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<[T; 2]> for Vector<T> {
|
||||
fn from([x, y]: [T; 2]) -> Self {
|
||||
Self::new(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Vector<T>> for [T; 2]
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
fn from(other: Vector<T>) -> Self {
|
||||
[other.x, other.y]
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
digraph G {
|
||||
fontname = "Roboto";
|
||||
newrank=true;
|
||||
node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"];
|
||||
edge[color="#333333"];
|
||||
|
||||
{ rank = same; iced_native iced_web }
|
||||
|
||||
iced_core -> iced_native [style=dashed];
|
||||
iced_core -> iced_web [style=dashed];
|
||||
|
||||
iced_core [style=dashed];
|
||||
}
|
Before Width: | Height: | Size: 13 KiB |
@ -1,56 +0,0 @@
|
||||
digraph G {
|
||||
fontname = "Roboto";
|
||||
newrank=true;
|
||||
node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"];
|
||||
edge[color="#333333"];
|
||||
|
||||
subgraph cluster_1 {
|
||||
label = "renderers ";
|
||||
labelloc = "b";
|
||||
labeljust = "r";
|
||||
fontcolor = "#0366d6";
|
||||
color="#f6f8fa";
|
||||
bgcolor="#f6f8fa";
|
||||
style=rounded;
|
||||
|
||||
etc_1 [label="...", style=solid, shape=none];
|
||||
iced_wgpu;
|
||||
}
|
||||
|
||||
subgraph cluster_2 {
|
||||
label = "shells ";
|
||||
labelloc = "b";
|
||||
labeljust = "r";
|
||||
fontcolor = "#0366d6";
|
||||
color="#f6f8fa";
|
||||
bgcolor="#f6f8fa";
|
||||
style=rounded;
|
||||
|
||||
etc_2 [label="...", style=solid, shape=none];
|
||||
iced_winit;
|
||||
}
|
||||
|
||||
subgraph cluster_3 {
|
||||
style=invis;
|
||||
margin=20;
|
||||
iced;
|
||||
}
|
||||
|
||||
{ rank = same; iced_native iced_web }
|
||||
{ rank = same; iced_wgpu iced_winit etc_1 etc_2 }
|
||||
|
||||
iced_core -> iced_native [style=dashed];
|
||||
iced_core -> iced_web [style=dashed];
|
||||
iced_native -> iced_wgpu;
|
||||
iced_native -> iced_winit;
|
||||
|
||||
iced_winit -> iced;
|
||||
iced_wgpu -> iced;
|
||||
iced_web -> iced;
|
||||
|
||||
iced -> "cross-platform application";
|
||||
|
||||
iced_core [style=dashed];
|
||||
|
||||
"cross-platform application" [shape=box, width=2.8, height=0.6];
|
||||
}
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 124 KiB |
BIN
docs/graphs/foundations.png
Normal file
After Width: | Height: | Size: 18 KiB |
@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
for file in *.dot
|
||||
do
|
||||
dot -Tpng ${file} -o ${file%.*}.png
|
||||
done
|
@ -1,46 +0,0 @@
|
||||
digraph G {
|
||||
fontname = "Roboto";
|
||||
newrank=true;
|
||||
node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"];
|
||||
edge[color="#333333"];
|
||||
|
||||
subgraph cluster_1 {
|
||||
label = "renderers ";
|
||||
labelloc = "b";
|
||||
labeljust = "r";
|
||||
fontcolor = "#0366d6";
|
||||
color="#f6f8fa";
|
||||
bgcolor="#f6f8fa";
|
||||
style=rounded;
|
||||
|
||||
etc_1 [label="...", style=solid, shape=none];
|
||||
iced_wgpu;
|
||||
}
|
||||
|
||||
subgraph cluster_2 {
|
||||
label = "shells ";
|
||||
labelloc = "b";
|
||||
labeljust = "r";
|
||||
fontcolor = "#0366d6";
|
||||
color="#f6f8fa";
|
||||
bgcolor="#f6f8fa";
|
||||
style=rounded;
|
||||
|
||||
etc_2 [label="...", style=solid, shape=none];
|
||||
iced_winit;
|
||||
}
|
||||
|
||||
subgraph cluster_3 {
|
||||
style=invis;
|
||||
margin=20;
|
||||
iced;
|
||||
}
|
||||
|
||||
{ rank = same; iced_wgpu iced_winit etc_1 etc_2 }
|
||||
|
||||
iced_winit -> iced;
|
||||
iced_wgpu -> iced;
|
||||
iced_web -> iced;
|
||||
|
||||
iced;
|
||||
}
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 98 KiB |
@ -1,41 +0,0 @@
|
||||
digraph G {
|
||||
fontname = "Roboto";
|
||||
newrank=true;
|
||||
node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"];
|
||||
edge[color="#333333"];
|
||||
|
||||
subgraph cluster_1 {
|
||||
label = "renderers ";
|
||||
labelloc = "b";
|
||||
labeljust = "r";
|
||||
fontcolor = "#0366d6";
|
||||
color="#f6f8fa";
|
||||
bgcolor="#f6f8fa";
|
||||
style=rounded;
|
||||
|
||||
etc_1 [label="...", style=solid, shape=none];
|
||||
iced_wgpu;
|
||||
}
|
||||
|
||||
subgraph cluster_2 {
|
||||
label = "shells ";
|
||||
labelloc = "b";
|
||||
labeljust = "r";
|
||||
fontcolor = "#0366d6";
|
||||
color="#f6f8fa";
|
||||
bgcolor="#f6f8fa";
|
||||
style=rounded;
|
||||
|
||||
etc_2 [label="...", style=solid, shape=none];
|
||||
iced_winit;
|
||||
}
|
||||
|
||||
|
||||
{ rank = same; iced_wgpu iced_winit etc_1 etc_2 }
|
||||
|
||||
iced_core -> iced_native [style=dashed];
|
||||
iced_native -> iced_wgpu;
|
||||
iced_native -> iced_winit;
|
||||
|
||||
iced_core [style=dashed];
|
||||
}
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 58 KiB |
@ -1,12 +0,0 @@
|
||||
digraph G {
|
||||
fontname = "Roboto";
|
||||
newrank=true;
|
||||
node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"];
|
||||
edge[color="#333333"];
|
||||
|
||||
iced_core -> iced_web [style=dashed];
|
||||
|
||||
iced_web -> iced;
|
||||
|
||||
iced_core [style=dashed];
|
||||
}
|
Before Width: | Height: | Size: 11 KiB |
@ -1,31 +0,0 @@
|
||||
digraph G {
|
||||
fontname = "Roboto";
|
||||
newrank=true;
|
||||
node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"];
|
||||
edge[color="#333333"];
|
||||
|
||||
subgraph cluster_1 {
|
||||
label = "renderers ";
|
||||
labelloc = "b";
|
||||
labeljust = "r";
|
||||
fontcolor = "#0366d6";
|
||||
color="#f6f8fa";
|
||||
bgcolor="#f6f8fa";
|
||||
style=rounded;
|
||||
|
||||
etc_1 [label="...", style=solid, shape=none];
|
||||
iced_wgpu;
|
||||
}
|
||||
|
||||
subgraph cluster_3 {
|
||||
style=invis;
|
||||
margin=20;
|
||||
iced;
|
||||
}
|
||||
|
||||
{ rank = same; iced_wgpu etc_1 }
|
||||
|
||||
iced_native -> iced_wgpu;
|
||||
|
||||
iced_wgpu -> iced;
|
||||
}
|
Before Width: | Height: | Size: 16 KiB |
@ -1,31 +0,0 @@
|
||||
digraph G {
|
||||
fontname = "Roboto";
|
||||
newrank=true;
|
||||
node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"];
|
||||
edge[color="#333333"];
|
||||
|
||||
subgraph cluster_2 {
|
||||
label = "shells ";
|
||||
labelloc = "b";
|
||||
labeljust = "r";
|
||||
fontcolor = "#0366d6";
|
||||
color="#f6f8fa";
|
||||
bgcolor="#f6f8fa";
|
||||
style=rounded;
|
||||
|
||||
etc_2 [label="...", style=solid, shape=none];
|
||||
iced_winit;
|
||||
}
|
||||
|
||||
subgraph cluster_3 {
|
||||
style=invis;
|
||||
margin=20;
|
||||
iced;
|
||||
}
|
||||
|
||||
{ rank = same; iced_winit etc_2 }
|
||||
|
||||
iced_native -> iced_winit;
|
||||
|
||||
iced_winit -> iced;
|
||||
}
|
Before Width: | Height: | Size: 16 KiB |
@ -4,11 +4,10 @@ you want to learn about a specific release, check out [the release list].
|
||||
|
||||
[the release list]: https://github.com/hecrj/iced/releases
|
||||
|
||||
## [Tour](tour.rs)
|
||||
|
||||
## [Tour](tour)
|
||||
A simple UI tour that can run both on native platforms and the web! It showcases different widgets that can be built using Iced.
|
||||
|
||||
The __[`tour`]__ file contains all the code of the example! All the cross-platform GUI is defined in terms of __state__, __messages__, __update logic__ and __view logic__.
|
||||
The __[`main`](tour/src/main.rs)__ file contains all the code of the example! All the cross-platform GUI is defined in terms of __state__, __messages__, __update logic__ and __view logic__.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/politeadorableiberianmole">
|
||||
@ -16,7 +15,6 @@ The __[`tour`]__ file contains all the code of the example! All the cross-platfo
|
||||
</a>
|
||||
</div>
|
||||
|
||||
[`tour`]: tour.rs
|
||||
[`iced_winit`]: ../winit
|
||||
[`iced_native`]: ../native
|
||||
[`iced_wgpu`]: ../wgpu
|
||||
@ -26,19 +24,17 @@ The __[`tour`]__ file contains all the code of the example! All the cross-platfo
|
||||
|
||||
You can run the native version with `cargo run`:
|
||||
```
|
||||
cargo run --example tour
|
||||
cargo run --package tour
|
||||
```
|
||||
|
||||
The web version can be run by following [the usage instructions of `iced_web`] or by accessing [iced.rs](https://iced.rs/)!
|
||||
|
||||
[the usage instructions of `iced_web`]: ../web#usage
|
||||
|
||||
## [Todos](todos)
|
||||
A todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them.
|
||||
|
||||
## [Todos](todos.rs)
|
||||
|
||||
A simple todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them.
|
||||
|
||||
All the example code is located in the __[`todos`]__ file.
|
||||
The example code is located in the __[`main`](todos/src/main.rs)__ file.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/littlesanehalicore">
|
||||
@ -48,16 +44,81 @@ All the example code is located in the __[`todos`]__ file.
|
||||
|
||||
You can run the native version with `cargo run`:
|
||||
```
|
||||
cargo run --example todos
|
||||
cargo run --package todos
|
||||
```
|
||||
We have not yet implemented a `LocalStorage` version of the auto-save feature. Therefore, it does not work on web _yet_!
|
||||
|
||||
[`todos`]: todos.rs
|
||||
[TodoMVC]: http://todomvc.com/
|
||||
|
||||
## [Coffee]
|
||||
## [Game of Life](game_of_life)
|
||||
An interactive version of the [Game of Life], invented by [John Horton Conway].
|
||||
|
||||
Since [Iced was born in May], it has been powering the user interfaces in
|
||||
It runs a simulation in a background thread while allowing interaction with a `Canvas` that displays an infinite grid with zooming, panning, and drawing support.
|
||||
|
||||
The relevant code is located in the __[`main`](game_of_life/src/main.rs)__ file.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/briefaccurateaardvark">
|
||||
<img src="https://thumbs.gfycat.com/BriefAccurateAardvark-size_restricted.gif">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package game_of_life
|
||||
```
|
||||
|
||||
[Game of Life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
|
||||
[John Horton Conway]: https://en.wikipedia.org/wiki/John_Horton_Conway
|
||||
|
||||
## [Styling](styling)
|
||||
An example showcasing custom styling with a light and dark theme.
|
||||
|
||||
The example code is located in the __[`main`](styling/src/main.rs)__ file.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://user-images.githubusercontent.com/518289/71867993-acff4300-310c-11ea-85a3-d01d8f884346.gif">
|
||||
<img src="https://user-images.githubusercontent.com/518289/71867993-acff4300-310c-11ea-85a3-d01d8f884346.gif" height="400px">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package styling
|
||||
```
|
||||
|
||||
## Extras
|
||||
A bunch of simpler examples exist:
|
||||
|
||||
- [`bezier_tool`](bezier_tool), a Paint-like tool for drawing Bézier curves using the `Canvas` widget.
|
||||
- [`clock`](clock), an application that uses the `Canvas` widget to draw a clock and its hands to display the current time.
|
||||
- [`color_palette`](color_palette), a color palette generator based on a user-defined root color.
|
||||
- [`counter`](counter), the classic counter example explained in the [`README`](../README.md).
|
||||
- [`custom_widget`](custom_widget), a demonstration of how to build a custom widget that draws a circle.
|
||||
- [`download_progress`](download_progress), a basic application that asynchronously downloads a dummy file of 100 MB and tracks the download progress.
|
||||
- [`events`](events), a log of native events displayed using a conditional `Subscription`.
|
||||
- [`geometry`](geometry), a custom widget showcasing how to draw geometry with the `Mesh2D` primitive in [`iced_wgpu`](../wgpu).
|
||||
- [`integration`](integration), a demonstration of how to integrate Iced in an existing graphical application.
|
||||
- [`pane_grid`](pane_grid), a grid of panes that can be split, resized, and reorganized.
|
||||
- [`pick_list`](pick_list), a dropdown list of selectable options.
|
||||
- [`pokedex`](pokedex), an application that displays a random Pokédex entry (sprite included!) by using the [PokéAPI].
|
||||
- [`progress_bar`](progress_bar), a simple progress bar that can be filled by using a slider.
|
||||
- [`scrollable`](scrollable), a showcase of the various scrollbar width options.
|
||||
- [`solar_system`](solar_system), an animated solar system drawn using the `Canvas` widget and showcasing how to compose different transforms.
|
||||
- [`stopwatch`](stopwatch), a watch with start/stop and reset buttons showcasing how to listen to time.
|
||||
- [`svg`](svg), an application that renders the [Ghostscript Tiger] by leveraging the `Svg` widget.
|
||||
|
||||
All of them are packaged in their own crate and, therefore, can be run using `cargo`:
|
||||
```
|
||||
cargo run --package <example>
|
||||
```
|
||||
|
||||
[`lyon`]: https://github.com/nical/lyon
|
||||
[PokéAPI]: https://pokeapi.co/
|
||||
[Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg
|
||||
|
||||
## [Coffee]
|
||||
Since [Iced was born in May 2019], it has been powering the user interfaces in
|
||||
[Coffee], an experimental 2D game engine.
|
||||
|
||||
|
||||
@ -67,6 +128,6 @@ Since [Iced was born in May], it has been powering the user interfaces in
|
||||
</a>
|
||||
</div>
|
||||
|
||||
[Iced was born in May]: https://github.com/hecrj/coffee/pull/35
|
||||
[Iced was born in May 2019]: https://github.com/hecrj/coffee/pull/35
|
||||
[`ui` module]: https://docs.rs/coffee/0.3.2/coffee/ui/index.html
|
||||
[Coffee]: https://github.com/hecrj/coffee
|
||||
|
9
examples/bezier_tool/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "bezier_tool"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["canvas"] }
|
18
examples/bezier_tool/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
## Bézier tool
|
||||
|
||||
A Paint-like tool for drawing Bézier curves using the `Canvas` widget.
|
||||
|
||||
The __[`main`]__ file contains all the code of the example.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/soulfulinfiniteantbear">
|
||||
<img src="https://thumbs.gfycat.com/SoulfulInfiniteAntbear-small.gif">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package bezier_tool
|
||||
```
|
||||
|
||||
[`main`]: src/main.rs
|
245
examples/bezier_tool/src/main.rs
Normal file
@ -0,0 +1,245 @@
|
||||
//! This example showcases an interactive `Canvas` for drawing Bézier curves.
|
||||
use iced::{
|
||||
button, Align, Button, Column, Element, Length, Sandbox, Settings, Text,
|
||||
};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
Example::run(Settings {
|
||||
antialiasing: true,
|
||||
..Settings::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Example {
|
||||
bezier: bezier::State,
|
||||
curves: Vec<bezier::Curve>,
|
||||
button_state: button::State,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Message {
|
||||
AddCurve(bezier::Curve),
|
||||
Clear,
|
||||
}
|
||||
|
||||
impl Sandbox for Example {
|
||||
type Message = Message;
|
||||
|
||||
fn new() -> Self {
|
||||
Example::default()
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Bezier tool - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::AddCurve(curve) => {
|
||||
self.curves.push(curve);
|
||||
self.bezier.request_redraw();
|
||||
}
|
||||
Message::Clear => {
|
||||
self.bezier = bezier::State::default();
|
||||
self.curves.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
Column::new()
|
||||
.padding(20)
|
||||
.spacing(20)
|
||||
.align_items(Align::Center)
|
||||
.push(
|
||||
Text::new("Bezier tool example")
|
||||
.width(Length::Shrink)
|
||||
.size(50),
|
||||
)
|
||||
.push(self.bezier.view(&self.curves).map(Message::AddCurve))
|
||||
.push(
|
||||
Button::new(&mut self.button_state, Text::new("Clear"))
|
||||
.padding(8)
|
||||
.on_press(Message::Clear),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
mod bezier {
|
||||
use iced::{
|
||||
canvas::event::{self, Event},
|
||||
canvas::{self, Canvas, Cursor, Frame, Geometry, Path, Stroke},
|
||||
mouse, Element, Length, Point, Rectangle,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
pending: Option<Pending>,
|
||||
cache: canvas::Cache,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn view<'a>(
|
||||
&'a mut self,
|
||||
curves: &'a [Curve],
|
||||
) -> Element<'a, Curve> {
|
||||
Canvas::new(Bezier {
|
||||
state: self,
|
||||
curves,
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn request_redraw(&mut self) {
|
||||
self.cache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
struct Bezier<'a> {
|
||||
state: &'a mut State,
|
||||
curves: &'a [Curve],
|
||||
}
|
||||
|
||||
impl<'a> canvas::Program<Curve> for Bezier<'a> {
|
||||
fn update(
|
||||
&mut self,
|
||||
event: Event,
|
||||
bounds: Rectangle,
|
||||
cursor: Cursor,
|
||||
) -> (event::Status, Option<Curve>) {
|
||||
let cursor_position =
|
||||
if let Some(position) = cursor.position_in(&bounds) {
|
||||
position
|
||||
} else {
|
||||
return (event::Status::Ignored, None);
|
||||
};
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse_event) => {
|
||||
let message = match mouse_event {
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left) => {
|
||||
match self.state.pending {
|
||||
None => {
|
||||
self.state.pending = Some(Pending::One {
|
||||
from: cursor_position,
|
||||
});
|
||||
|
||||
None
|
||||
}
|
||||
Some(Pending::One { from }) => {
|
||||
self.state.pending = Some(Pending::Two {
|
||||
from,
|
||||
to: cursor_position,
|
||||
});
|
||||
|
||||
None
|
||||
}
|
||||
Some(Pending::Two { from, to }) => {
|
||||
self.state.pending = None;
|
||||
|
||||
Some(Curve {
|
||||
from,
|
||||
to,
|
||||
control: cursor_position,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
(event::Status::Captured, message)
|
||||
}
|
||||
_ => (event::Status::Ignored, None),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> {
|
||||
let content =
|
||||
self.state.cache.draw(bounds.size(), |frame: &mut Frame| {
|
||||
Curve::draw_all(self.curves, frame);
|
||||
|
||||
frame.stroke(
|
||||
&Path::rectangle(Point::ORIGIN, frame.size()),
|
||||
Stroke::default(),
|
||||
);
|
||||
});
|
||||
|
||||
if let Some(pending) = &self.state.pending {
|
||||
let pending_curve = pending.draw(bounds, cursor);
|
||||
|
||||
vec![content, pending_curve]
|
||||
} else {
|
||||
vec![content]
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
bounds: Rectangle,
|
||||
cursor: Cursor,
|
||||
) -> mouse::Interaction {
|
||||
if cursor.is_over(&bounds) {
|
||||
mouse::Interaction::Crosshair
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Curve {
|
||||
from: Point,
|
||||
to: Point,
|
||||
control: Point,
|
||||
}
|
||||
|
||||
impl Curve {
|
||||
fn draw_all(curves: &[Curve], frame: &mut Frame) {
|
||||
let curves = Path::new(|p| {
|
||||
for curve in curves {
|
||||
p.move_to(curve.from);
|
||||
p.quadratic_curve_to(curve.control, curve.to);
|
||||
}
|
||||
});
|
||||
|
||||
frame.stroke(&curves, Stroke::default().with_width(2.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Pending {
|
||||
One { from: Point },
|
||||
Two { from: Point, to: Point },
|
||||
}
|
||||
|
||||
impl Pending {
|
||||
fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Geometry {
|
||||
let mut frame = Frame::new(bounds.size());
|
||||
|
||||
if let Some(cursor_position) = cursor.position_in(&bounds) {
|
||||
match *self {
|
||||
Pending::One { from } => {
|
||||
let line = Path::line(from, cursor_position);
|
||||
frame.stroke(&line, Stroke::default().with_width(2.0));
|
||||
}
|
||||
Pending::Two { from, to } => {
|
||||
let curve = Curve {
|
||||
from,
|
||||
to,
|
||||
control: cursor_position,
|
||||
};
|
||||
|
||||
Curve::draw_all(&[curve], &mut frame);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
frame.into_geometry()
|
||||
}
|
||||
}
|
||||
}
|
10
examples/clock/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "clock"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
|
||||
chrono = "0.4"
|
16
examples/clock/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
## Clock
|
||||
|
||||
An application that uses the `Canvas` widget to draw a clock and its hands to display the current time.
|
||||
|
||||
The __[`main`]__ file contains all the code of the example.
|
||||
|
||||
<div align="center">
|
||||
<img src="https://user-images.githubusercontent.com/518289/74716344-a3e6b300-522e-11ea-8aea-3cc0a5100a2e.gif">
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package clock
|
||||
```
|
||||
|
||||
[`main`]: src/main.rs
|
137
examples/clock/src/main.rs
Normal file
@ -0,0 +1,137 @@
|
||||
use iced::{
|
||||
canvas::{self, Cache, Canvas, Cursor, Geometry, LineCap, Path, Stroke},
|
||||
executor, time, Application, Clipboard, Color, Command, Container, Element,
|
||||
Length, Point, Rectangle, Settings, Subscription, Vector,
|
||||
};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
Clock::run(Settings {
|
||||
antialiasing: true,
|
||||
..Settings::default()
|
||||
})
|
||||
}
|
||||
|
||||
struct Clock {
|
||||
now: chrono::DateTime<chrono::Local>,
|
||||
clock: Cache,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Message {
|
||||
Tick(chrono::DateTime<chrono::Local>),
|
||||
}
|
||||
|
||||
impl Application for Clock {
|
||||
type Executor = executor::Default;
|
||||
type Message = Message;
|
||||
type Flags = ();
|
||||
|
||||
fn new(_flags: ()) -> (Self, Command<Message>) {
|
||||
(
|
||||
Clock {
|
||||
now: chrono::Local::now(),
|
||||
clock: Default::default(),
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Clock - Iced")
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Message,
|
||||
_clipboard: &mut Clipboard,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::Tick(local_time) => {
|
||||
let now = local_time;
|
||||
|
||||
if now != self.now {
|
||||
self.now = now;
|
||||
self.clock.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
time::every(std::time::Duration::from_millis(500))
|
||||
.map(|_| Message::Tick(chrono::Local::now()))
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let canvas = Canvas::new(self)
|
||||
.width(Length::Units(400))
|
||||
.height(Length::Units(400));
|
||||
|
||||
Container::new(canvas)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(20)
|
||||
.center_x()
|
||||
.center_y()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl canvas::Program<Message> for Clock {
|
||||
fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> {
|
||||
use chrono::Timelike;
|
||||
|
||||
let clock = self.clock.draw(bounds.size(), |frame| {
|
||||
let center = frame.center();
|
||||
let radius = frame.width().min(frame.height()) / 2.0;
|
||||
|
||||
let background = Path::circle(center, radius);
|
||||
frame.fill(&background, Color::from_rgb8(0x12, 0x93, 0xD8));
|
||||
|
||||
let short_hand =
|
||||
Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius));
|
||||
|
||||
let long_hand =
|
||||
Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius));
|
||||
|
||||
let thin_stroke = Stroke {
|
||||
width: radius / 100.0,
|
||||
color: Color::WHITE,
|
||||
line_cap: LineCap::Round,
|
||||
..Stroke::default()
|
||||
};
|
||||
|
||||
let wide_stroke = Stroke {
|
||||
width: thin_stroke.width * 3.0,
|
||||
..thin_stroke
|
||||
};
|
||||
|
||||
frame.translate(Vector::new(center.x, center.y));
|
||||
|
||||
frame.with_save(|frame| {
|
||||
frame.rotate(hand_rotation(self.now.hour(), 12));
|
||||
frame.stroke(&short_hand, wide_stroke);
|
||||
});
|
||||
|
||||
frame.with_save(|frame| {
|
||||
frame.rotate(hand_rotation(self.now.minute(), 60));
|
||||
frame.stroke(&long_hand, wide_stroke);
|
||||
});
|
||||
|
||||
frame.with_save(|frame| {
|
||||
frame.rotate(hand_rotation(self.now.second(), 60));
|
||||
frame.stroke(&long_hand, thin_stroke);
|
||||
})
|
||||
});
|
||||
|
||||
vec![clock]
|
||||
}
|
||||
}
|
||||
|
||||
fn hand_rotation(n: u32, total: u32) -> f32 {
|
||||
let turns = n as f32 / total as f32;
|
||||
|
||||
2.0 * std::f32::consts::PI * turns
|
||||
}
|
10
examples/color_palette/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "color_palette"
|
||||
version = "0.1.0"
|
||||
authors = ["Clark Moody <clark@clarkmoody.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["canvas", "palette"] }
|
||||
palette = "0.5.0"
|
15
examples/color_palette/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
## Color palette
|
||||
|
||||
A color palette generator, based on a user-defined root color.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/dirtylonebighornsheep">
|
||||
<img src="https://github.com/hecrj/iced/raw/1a8d253611d3796b0a32b2f096bb54565a5292e0/examples/color_palette/screenshot.png">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
|
||||
```
|
||||
cargo run --package color_palette
|
||||
```
|
BIN
examples/color_palette/screenshot.png
Normal file
After Width: | Height: | Size: 103 KiB |
462
examples/color_palette/src/main.rs
Normal file
@ -0,0 +1,462 @@
|
||||
use iced::canvas::{self, Cursor, Frame, Geometry, Path};
|
||||
use iced::{
|
||||
slider, Align, Canvas, Color, Column, Element, HorizontalAlignment, Length,
|
||||
Point, Rectangle, Row, Sandbox, Settings, Size, Slider, Text, Vector,
|
||||
VerticalAlignment,
|
||||
};
|
||||
use palette::{self, Hsl, Limited, Srgb};
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
ColorPalette::run(Settings {
|
||||
antialiasing: true,
|
||||
..Settings::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ColorPalette {
|
||||
theme: Theme,
|
||||
rgb: ColorPicker<Color>,
|
||||
hsl: ColorPicker<palette::Hsl>,
|
||||
hsv: ColorPicker<palette::Hsv>,
|
||||
hwb: ColorPicker<palette::Hwb>,
|
||||
lab: ColorPicker<palette::Lab>,
|
||||
lch: ColorPicker<palette::Lch>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Message {
|
||||
RgbColorChanged(Color),
|
||||
HslColorChanged(palette::Hsl),
|
||||
HsvColorChanged(palette::Hsv),
|
||||
HwbColorChanged(palette::Hwb),
|
||||
LabColorChanged(palette::Lab),
|
||||
LchColorChanged(palette::Lch),
|
||||
}
|
||||
|
||||
impl Sandbox for ColorPalette {
|
||||
type Message = Message;
|
||||
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Color palette - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) {
|
||||
let srgb = match message {
|
||||
Message::RgbColorChanged(rgb) => palette::Srgb::from(rgb),
|
||||
Message::HslColorChanged(hsl) => palette::Srgb::from(hsl),
|
||||
Message::HsvColorChanged(hsv) => palette::Srgb::from(hsv),
|
||||
Message::HwbColorChanged(hwb) => palette::Srgb::from(hwb),
|
||||
Message::LabColorChanged(lab) => palette::Srgb::from(lab),
|
||||
Message::LchColorChanged(lch) => palette::Srgb::from(lch),
|
||||
};
|
||||
|
||||
self.theme = Theme::new(srgb.clamp());
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let base = self.theme.base;
|
||||
|
||||
let srgb = palette::Srgb::from(base);
|
||||
let hsl = palette::Hsl::from(srgb);
|
||||
let hsv = palette::Hsv::from(srgb);
|
||||
let hwb = palette::Hwb::from(srgb);
|
||||
let lab = palette::Lab::from(srgb);
|
||||
let lch = palette::Lch::from(srgb);
|
||||
|
||||
Column::new()
|
||||
.padding(10)
|
||||
.spacing(10)
|
||||
.push(self.rgb.view(base).map(Message::RgbColorChanged))
|
||||
.push(self.hsl.view(hsl).map(Message::HslColorChanged))
|
||||
.push(self.hsv.view(hsv).map(Message::HsvColorChanged))
|
||||
.push(self.hwb.view(hwb).map(Message::HwbColorChanged))
|
||||
.push(self.lab.view(lab).map(Message::LabColorChanged))
|
||||
.push(self.lch.view(lch).map(Message::LchColorChanged))
|
||||
.push(self.theme.view())
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Theme {
|
||||
lower: Vec<Color>,
|
||||
base: Color,
|
||||
higher: Vec<Color>,
|
||||
canvas_cache: canvas::Cache,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn new(base: impl Into<Color>) -> Theme {
|
||||
use palette::{Hue, Shade};
|
||||
|
||||
let base = base.into();
|
||||
|
||||
// Convert to HSL color for manipulation
|
||||
let hsl = Hsl::from(Srgb::from(base));
|
||||
|
||||
let lower = [
|
||||
hsl.shift_hue(-135.0).lighten(0.075),
|
||||
hsl.shift_hue(-120.0),
|
||||
hsl.shift_hue(-105.0).darken(0.075),
|
||||
hsl.darken(0.075),
|
||||
];
|
||||
|
||||
let higher = [
|
||||
hsl.lighten(0.075),
|
||||
hsl.shift_hue(105.0).darken(0.075),
|
||||
hsl.shift_hue(120.0),
|
||||
hsl.shift_hue(135.0).lighten(0.075),
|
||||
];
|
||||
|
||||
Theme {
|
||||
lower: lower
|
||||
.iter()
|
||||
.map(|&color| Srgb::from(color).clamp().into())
|
||||
.collect(),
|
||||
base,
|
||||
higher: higher
|
||||
.iter()
|
||||
.map(|&color| Srgb::from(color).clamp().into())
|
||||
.collect(),
|
||||
canvas_cache: canvas::Cache::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.lower.len() + self.higher.len() + 1
|
||||
}
|
||||
|
||||
pub fn colors(&self) -> impl Iterator<Item = &Color> {
|
||||
self.lower
|
||||
.iter()
|
||||
.chain(std::iter::once(&self.base))
|
||||
.chain(self.higher.iter())
|
||||
}
|
||||
|
||||
pub fn view(&mut self) -> Element<Message> {
|
||||
Canvas::new(self)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
let pad = 20.0;
|
||||
|
||||
let box_size = Size {
|
||||
width: frame.width() / self.len() as f32,
|
||||
height: frame.height() / 2.0 - pad,
|
||||
};
|
||||
|
||||
let triangle = Path::new(|path| {
|
||||
path.move_to(Point { x: 0.0, y: -0.5 });
|
||||
path.line_to(Point { x: -0.5, y: 0.0 });
|
||||
path.line_to(Point { x: 0.5, y: 0.0 });
|
||||
path.close();
|
||||
});
|
||||
|
||||
let mut text = canvas::Text {
|
||||
horizontal_alignment: HorizontalAlignment::Center,
|
||||
vertical_alignment: VerticalAlignment::Top,
|
||||
size: 15.0,
|
||||
..canvas::Text::default()
|
||||
};
|
||||
|
||||
for (i, &color) in self.colors().enumerate() {
|
||||
let anchor = Point {
|
||||
x: (i as f32) * box_size.width,
|
||||
y: 0.0,
|
||||
};
|
||||
frame.fill_rectangle(anchor, box_size, color);
|
||||
|
||||
// We show a little indicator for the base color
|
||||
if color == self.base {
|
||||
let triangle_x = anchor.x + box_size.width / 2.0;
|
||||
|
||||
frame.with_save(|frame| {
|
||||
frame.translate(Vector::new(triangle_x, 0.0));
|
||||
frame.scale(10.0);
|
||||
frame.rotate(std::f32::consts::PI);
|
||||
|
||||
frame.fill(&triangle, Color::WHITE);
|
||||
});
|
||||
|
||||
frame.with_save(|frame| {
|
||||
frame.translate(Vector::new(triangle_x, box_size.height));
|
||||
frame.scale(10.0);
|
||||
|
||||
frame.fill(&triangle, Color::WHITE);
|
||||
});
|
||||
}
|
||||
|
||||
frame.fill_text(canvas::Text {
|
||||
content: color_hex_string(&color),
|
||||
position: Point {
|
||||
x: anchor.x + box_size.width / 2.0,
|
||||
y: box_size.height,
|
||||
},
|
||||
..text
|
||||
});
|
||||
}
|
||||
|
||||
text.vertical_alignment = VerticalAlignment::Bottom;
|
||||
|
||||
let hsl = Hsl::from(Srgb::from(self.base));
|
||||
for i in 0..self.len() {
|
||||
let pct = (i as f32 + 1.0) / (self.len() as f32 + 1.0);
|
||||
let graded = Hsl {
|
||||
lightness: 1.0 - pct,
|
||||
..hsl
|
||||
};
|
||||
let color: Color = Srgb::from(graded.clamp()).into();
|
||||
|
||||
let anchor = Point {
|
||||
x: (i as f32) * box_size.width,
|
||||
y: box_size.height + 2.0 * pad,
|
||||
};
|
||||
|
||||
frame.fill_rectangle(anchor, box_size, color);
|
||||
|
||||
frame.fill_text(canvas::Text {
|
||||
content: color_hex_string(&color),
|
||||
position: Point {
|
||||
x: anchor.x + box_size.width / 2.0,
|
||||
y: box_size.height + 2.0 * pad,
|
||||
},
|
||||
..text
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl canvas::Program<Message> for Theme {
|
||||
fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> {
|
||||
let theme = self.canvas_cache.draw(bounds.size(), |frame| {
|
||||
self.draw(frame);
|
||||
});
|
||||
|
||||
vec![theme]
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Theme {
|
||||
fn default() -> Self {
|
||||
Theme::new(Color::from_rgb8(75, 128, 190))
|
||||
}
|
||||
}
|
||||
|
||||
fn color_hex_string(color: &Color) -> String {
|
||||
format!(
|
||||
"#{:x}{:x}{:x}",
|
||||
(255.0 * color.r).round() as u8,
|
||||
(255.0 * color.g).round() as u8,
|
||||
(255.0 * color.b).round() as u8
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ColorPicker<C: ColorSpace> {
|
||||
sliders: [slider::State; 3],
|
||||
color_space: PhantomData<C>,
|
||||
}
|
||||
|
||||
trait ColorSpace: Sized {
|
||||
const LABEL: &'static str;
|
||||
const COMPONENT_RANGES: [RangeInclusive<f64>; 3];
|
||||
|
||||
fn new(a: f32, b: f32, c: f32) -> Self;
|
||||
|
||||
fn components(&self) -> [f32; 3];
|
||||
|
||||
fn to_string(&self) -> String;
|
||||
}
|
||||
|
||||
impl<C: 'static + ColorSpace + Copy> ColorPicker<C> {
|
||||
fn view(&mut self, color: C) -> Element<C> {
|
||||
let [c1, c2, c3] = color.components();
|
||||
let [s1, s2, s3] = &mut self.sliders;
|
||||
let [cr1, cr2, cr3] = C::COMPONENT_RANGES;
|
||||
|
||||
fn slider<C: Clone>(
|
||||
state: &mut slider::State,
|
||||
range: RangeInclusive<f64>,
|
||||
component: f32,
|
||||
update: impl Fn(f32) -> C + 'static,
|
||||
) -> Slider<f64, C> {
|
||||
Slider::new(state, range, f64::from(component), move |v| {
|
||||
update(v as f32)
|
||||
})
|
||||
.step(0.01)
|
||||
}
|
||||
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.align_items(Align::Center)
|
||||
.push(Text::new(C::LABEL).width(Length::Units(50)))
|
||||
.push(slider(s1, cr1, c1, move |v| C::new(v, c2, c3)))
|
||||
.push(slider(s2, cr2, c2, move |v| C::new(c1, v, c3)))
|
||||
.push(slider(s3, cr3, c3, move |v| C::new(c1, c2, v)))
|
||||
.push(
|
||||
Text::new(color.to_string())
|
||||
.width(Length::Units(185))
|
||||
.size(14),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorSpace for Color {
|
||||
const LABEL: &'static str = "RGB";
|
||||
const COMPONENT_RANGES: [RangeInclusive<f64>; 3] =
|
||||
[0.0..=1.0, 0.0..=1.0, 0.0..=1.0];
|
||||
|
||||
fn new(r: f32, g: f32, b: f32) -> Self {
|
||||
Color::from_rgb(r, g, b)
|
||||
}
|
||||
|
||||
fn components(&self) -> [f32; 3] {
|
||||
[self.r, self.g, self.b]
|
||||
}
|
||||
|
||||
fn to_string(&self) -> String {
|
||||
format!(
|
||||
"rgb({:.0}, {:.0}, {:.0})",
|
||||
255.0 * self.r,
|
||||
255.0 * self.g,
|
||||
255.0 * self.b
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorSpace for palette::Hsl {
|
||||
const LABEL: &'static str = "HSL";
|
||||
const COMPONENT_RANGES: [RangeInclusive<f64>; 3] =
|
||||
[0.0..=360.0, 0.0..=1.0, 0.0..=1.0];
|
||||
|
||||
fn new(hue: f32, saturation: f32, lightness: f32) -> Self {
|
||||
palette::Hsl::new(
|
||||
palette::RgbHue::from_degrees(hue),
|
||||
saturation,
|
||||
lightness,
|
||||
)
|
||||
}
|
||||
|
||||
fn components(&self) -> [f32; 3] {
|
||||
[
|
||||
self.hue.to_positive_degrees(),
|
||||
self.saturation,
|
||||
self.lightness,
|
||||
]
|
||||
}
|
||||
|
||||
fn to_string(&self) -> String {
|
||||
format!(
|
||||
"hsl({:.1}, {:.1}%, {:.1}%)",
|
||||
self.hue.to_positive_degrees(),
|
||||
100.0 * self.saturation,
|
||||
100.0 * self.lightness
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorSpace for palette::Hsv {
|
||||
const LABEL: &'static str = "HSV";
|
||||
const COMPONENT_RANGES: [RangeInclusive<f64>; 3] =
|
||||
[0.0..=360.0, 0.0..=1.0, 0.0..=1.0];
|
||||
|
||||
fn new(hue: f32, saturation: f32, value: f32) -> Self {
|
||||
palette::Hsv::new(palette::RgbHue::from_degrees(hue), saturation, value)
|
||||
}
|
||||
|
||||
fn components(&self) -> [f32; 3] {
|
||||
[self.hue.to_positive_degrees(), self.saturation, self.value]
|
||||
}
|
||||
|
||||
fn to_string(&self) -> String {
|
||||
format!(
|
||||
"hsv({:.1}, {:.1}%, {:.1}%)",
|
||||
self.hue.to_positive_degrees(),
|
||||
100.0 * self.saturation,
|
||||
100.0 * self.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorSpace for palette::Hwb {
|
||||
const LABEL: &'static str = "HWB";
|
||||
const COMPONENT_RANGES: [RangeInclusive<f64>; 3] =
|
||||
[0.0..=360.0, 0.0..=1.0, 0.0..=1.0];
|
||||
|
||||
fn new(hue: f32, whiteness: f32, blackness: f32) -> Self {
|
||||
palette::Hwb::new(
|
||||
palette::RgbHue::from_degrees(hue),
|
||||
whiteness,
|
||||
blackness,
|
||||
)
|
||||
}
|
||||
|
||||
fn components(&self) -> [f32; 3] {
|
||||
[
|
||||
self.hue.to_positive_degrees(),
|
||||
self.whiteness,
|
||||
self.blackness,
|
||||
]
|
||||
}
|
||||
|
||||
fn to_string(&self) -> String {
|
||||
format!(
|
||||
"hwb({:.1}, {:.1}%, {:.1}%)",
|
||||
self.hue.to_positive_degrees(),
|
||||
100.0 * self.whiteness,
|
||||
100.0 * self.blackness
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorSpace for palette::Lab {
|
||||
const LABEL: &'static str = "Lab";
|
||||
const COMPONENT_RANGES: [RangeInclusive<f64>; 3] =
|
||||
[0.0..=100.0, -128.0..=127.0, -128.0..=127.0];
|
||||
|
||||
fn new(l: f32, a: f32, b: f32) -> Self {
|
||||
palette::Lab::new(l, a, b)
|
||||
}
|
||||
|
||||
fn components(&self) -> [f32; 3] {
|
||||
[self.l, self.a, self.b]
|
||||
}
|
||||
|
||||
fn to_string(&self) -> String {
|
||||
format!("Lab({:.1}, {:.1}, {:.1})", self.l, self.a, self.b)
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorSpace for palette::Lch {
|
||||
const LABEL: &'static str = "Lch";
|
||||
const COMPONENT_RANGES: [RangeInclusive<f64>; 3] =
|
||||
[0.0..=100.0, 0.0..=128.0, 0.0..=360.0];
|
||||
|
||||
fn new(l: f32, chroma: f32, hue: f32) -> Self {
|
||||
palette::Lch::new(l, chroma, palette::LabHue::from_degrees(hue))
|
||||
}
|
||||
|
||||
fn components(&self) -> [f32; 3] {
|
||||
[self.l, self.chroma, self.hue.to_positive_degrees()]
|
||||
}
|
||||
|
||||
fn to_string(&self) -> String {
|
||||
format!(
|
||||
"Lch({:.1}, {:.1}, {:.1})",
|
||||
self.l,
|
||||
self.chroma,
|
||||
self.hue.to_positive_degrees()
|
||||
)
|
||||
}
|
||||
}
|
9
examples/counter/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "counter"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../.." }
|
18
examples/counter/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
## Counter
|
||||
|
||||
The classic counter example explained in the [`README`](../../README.md).
|
||||
|
||||
The __[`main`]__ file contains all the code of the example.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/fairdeadcatbird">
|
||||
<img src="https://thumbs.gfycat.com/FairDeadCatbird-small.gif">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package counter
|
||||
```
|
||||
|
||||
[`main`]: src/main.rs
|
57
examples/counter/src/main.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use iced::{button, Align, Button, Column, Element, Sandbox, Settings, Text};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
Counter::run(Settings::default())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Counter {
|
||||
value: i32,
|
||||
increment_button: button::State,
|
||||
decrement_button: button::State,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Message {
|
||||
IncrementPressed,
|
||||
DecrementPressed,
|
||||
}
|
||||
|
||||
impl Sandbox for Counter {
|
||||
type Message = Message;
|
||||
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Counter - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::IncrementPressed => {
|
||||
self.value += 1;
|
||||
}
|
||||
Message::DecrementPressed => {
|
||||
self.value -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
Column::new()
|
||||
.padding(20)
|
||||
.align_items(Align::Center)
|
||||
.push(
|
||||
Button::new(&mut self.increment_button, Text::new("Increment"))
|
||||
.on_press(Message::IncrementPressed),
|
||||
)
|
||||
.push(Text::new(self.value.to_string()).size(50))
|
||||
.push(
|
||||
Button::new(&mut self.decrement_button, Text::new("Decrement"))
|
||||
.on_press(Message::DecrementPressed),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
11
examples/custom_widget/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "custom_widget"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../.." }
|
||||
iced_native = { path = "../../native" }
|
||||
iced_graphics = { path = "../../graphics" }
|
18
examples/custom_widget/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
## Custom widget
|
||||
|
||||
A demonstration of how to build a custom widget that draws a circle.
|
||||
|
||||
The __[`main`]__ file contains all the code of the example.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/jealouscornyhomalocephale">
|
||||
<img src="https://thumbs.gfycat.com/JealousCornyHomalocephale-small.gif">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package custom_widget
|
||||
```
|
||||
|
||||
[`main`]: src/main.rs
|
152
examples/custom_widget/src/main.rs
Normal file
@ -0,0 +1,152 @@
|
||||
//! This example showcases a simple native custom widget that draws a circle.
|
||||
mod circle {
|
||||
// For now, to implement a custom native widget you will need to add
|
||||
// `iced_native` and `iced_wgpu` to your dependencies.
|
||||
//
|
||||
// Then, you simply need to define your widget type and implement the
|
||||
// `iced_native::Widget` trait with the `iced_wgpu::Renderer`.
|
||||
//
|
||||
// Of course, you can choose to make the implementation renderer-agnostic,
|
||||
// if you wish to, by creating your own `Renderer` trait, which could be
|
||||
// implemented by `iced_wgpu` and other renderers.
|
||||
use iced_graphics::{Backend, Defaults, Primitive, Renderer};
|
||||
use iced_native::{
|
||||
layout, mouse, Background, Color, Element, Hasher, Layout, Length,
|
||||
Point, Rectangle, Size, Widget,
|
||||
};
|
||||
|
||||
pub struct Circle {
|
||||
radius: f32,
|
||||
}
|
||||
|
||||
impl Circle {
|
||||
pub fn new(radius: f32) -> Self {
|
||||
Self { radius }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, B> Widget<Message, Renderer<B>> for Circle
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer<B>,
|
||||
_limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout::Node::new(Size::new(self.radius * 2.0, self.radius * 2.0))
|
||||
}
|
||||
|
||||
fn hash_layout(&self, state: &mut Hasher) {
|
||||
use std::hash::Hash;
|
||||
|
||||
self.radius.to_bits().hash(state);
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_renderer: &mut Renderer<B>,
|
||||
_defaults: &Defaults,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) -> (Primitive, mouse::Interaction) {
|
||||
(
|
||||
Primitive::Quad {
|
||||
bounds: layout.bounds(),
|
||||
background: Background::Color(Color::BLACK),
|
||||
border_radius: self.radius,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
mouse::Interaction::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for Circle
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn into(self) -> Element<'a, Message, Renderer<B>> {
|
||||
Element::new(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use circle::Circle;
|
||||
use iced::{
|
||||
slider, Align, Column, Container, Element, Length, Sandbox, Settings,
|
||||
Slider, Text,
|
||||
};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
Example::run(Settings::default())
|
||||
}
|
||||
|
||||
struct Example {
|
||||
radius: f32,
|
||||
slider: slider::State,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Message {
|
||||
RadiusChanged(f32),
|
||||
}
|
||||
|
||||
impl Sandbox for Example {
|
||||
type Message = Message;
|
||||
|
||||
fn new() -> Self {
|
||||
Example {
|
||||
radius: 50.0,
|
||||
slider: slider::State::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Custom widget - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::RadiusChanged(radius) => {
|
||||
self.radius = radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let content = Column::new()
|
||||
.padding(20)
|
||||
.spacing(20)
|
||||
.max_width(500)
|
||||
.align_items(Align::Center)
|
||||
.push(Circle::new(self.radius))
|
||||
.push(Text::new(format!("Radius: {:.2}", self.radius)))
|
||||
.push(
|
||||
Slider::new(
|
||||
&mut self.slider,
|
||||
1.0..=100.0,
|
||||
self.radius,
|
||||
Message::RadiusChanged,
|
||||
)
|
||||
.step(0.01),
|
||||
);
|
||||
|
||||
Container::new(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_x()
|
||||
.center_y()
|
||||
.into()
|
||||
}
|
||||
}
|
12
examples/download_progress/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "download_progress"
|
||||
version = "0.1.0"
|
||||
authors = ["Songtronix <contact@songtronix.com>", "Folyd <lyshuhow@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["tokio"] }
|
||||
iced_native = { path = "../../native" }
|
||||
iced_futures = { path = "../../futures" }
|
||||
reqwest = "0.11"
|
17
examples/download_progress/README.md
Normal file
@ -0,0 +1,17 @@
|
||||
## Download progress
|
||||
|
||||
A basic application that asynchronously downloads multiple dummy files of 100 MB and tracks the download progress.
|
||||
|
||||
The example implements a custom `Subscription` in the __[`download`](src/download.rs)__ module. This subscription downloads and produces messages that can be used to keep track of its progress.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/wildearlyafricanwilddog">
|
||||
<img src="https://thumbs.gfycat.com/WildEarlyAfricanwilddog-small.gif">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
|
||||
```
|
||||
cargo run --package download_progress
|
||||
```
|
128
examples/download_progress/src/download.rs
Normal file
@ -0,0 +1,128 @@
|
||||
use iced_futures::futures;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
// Just a little utility function
|
||||
pub fn file<I: 'static + Hash + Copy + Send, T: ToString>(
|
||||
id: I,
|
||||
url: T,
|
||||
) -> iced::Subscription<(I, Progress)> {
|
||||
iced::Subscription::from_recipe(Download {
|
||||
id,
|
||||
url: url.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Download<I> {
|
||||
id: I,
|
||||
url: String,
|
||||
}
|
||||
|
||||
// Make sure iced can use our download stream
|
||||
impl<H, I, T> iced_native::subscription::Recipe<H, I> for Download<T>
|
||||
where
|
||||
T: 'static + Hash + Copy + Send,
|
||||
H: Hasher,
|
||||
{
|
||||
type Output = (T, Progress);
|
||||
|
||||
fn hash(&self, state: &mut H) {
|
||||
struct Marker;
|
||||
std::any::TypeId::of::<Marker>().hash(state);
|
||||
|
||||
self.id.hash(state);
|
||||
}
|
||||
|
||||
fn stream(
|
||||
self: Box<Self>,
|
||||
_input: futures::stream::BoxStream<'static, I>,
|
||||
) -> futures::stream::BoxStream<'static, Self::Output> {
|
||||
let id = self.id;
|
||||
|
||||
Box::pin(futures::stream::unfold(
|
||||
State::Ready(self.url),
|
||||
move |state| async move {
|
||||
match state {
|
||||
State::Ready(url) => {
|
||||
let response = reqwest::get(&url).await;
|
||||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
if let Some(total) = response.content_length() {
|
||||
Some((
|
||||
(id, Progress::Started),
|
||||
State::Downloading {
|
||||
response,
|
||||
total,
|
||||
downloaded: 0,
|
||||
},
|
||||
))
|
||||
} else {
|
||||
Some((
|
||||
(id, Progress::Errored),
|
||||
State::Finished,
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
Some(((id, Progress::Errored), State::Finished))
|
||||
}
|
||||
}
|
||||
}
|
||||
State::Downloading {
|
||||
mut response,
|
||||
total,
|
||||
downloaded,
|
||||
} => match response.chunk().await {
|
||||
Ok(Some(chunk)) => {
|
||||
let downloaded = downloaded + chunk.len() as u64;
|
||||
|
||||
let percentage =
|
||||
(downloaded as f32 / total as f32) * 100.0;
|
||||
|
||||
Some((
|
||||
(id, Progress::Advanced(percentage)),
|
||||
State::Downloading {
|
||||
response,
|
||||
total,
|
||||
downloaded,
|
||||
},
|
||||
))
|
||||
}
|
||||
Ok(None) => {
|
||||
Some(((id, Progress::Finished), State::Finished))
|
||||
}
|
||||
Err(_) => {
|
||||
Some(((id, Progress::Errored), State::Finished))
|
||||
}
|
||||
},
|
||||
State::Finished => {
|
||||
// We do not let the stream die, as it would start a
|
||||
// new download repeatedly if the user is not careful
|
||||
// in case of errors.
|
||||
let _: () = iced::futures::future::pending().await;
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Progress {
|
||||
Started,
|
||||
Advanced(f32),
|
||||
Finished,
|
||||
Errored,
|
||||
}
|
||||
|
||||
pub enum State {
|
||||
Ready(String),
|
||||
Downloading {
|
||||
response: reqwest::Response,
|
||||
total: u64,
|
||||
downloaded: u64,
|
||||
},
|
||||
Finished,
|
||||
}
|
219
examples/download_progress/src/main.rs
Normal file
@ -0,0 +1,219 @@
|
||||
use iced::{
|
||||
button, executor, Align, Application, Button, Clipboard, Column, Command,
|
||||
Container, Element, Length, ProgressBar, Settings, Subscription, Text,
|
||||
};
|
||||
|
||||
mod download;
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
Example::run(Settings::default())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Example {
|
||||
downloads: Vec<Download>,
|
||||
last_id: usize,
|
||||
add: button::State,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
Add,
|
||||
Download(usize),
|
||||
DownloadProgressed((usize, download::Progress)),
|
||||
}
|
||||
|
||||
impl Application for Example {
|
||||
type Executor = executor::Default;
|
||||
type Message = Message;
|
||||
type Flags = ();
|
||||
|
||||
fn new(_flags: ()) -> (Example, Command<Message>) {
|
||||
(
|
||||
Example {
|
||||
downloads: vec![Download::new(0)],
|
||||
last_id: 0,
|
||||
add: button::State::new(),
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Download progress - Iced")
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Message,
|
||||
_clipboard: &mut Clipboard,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::Add => {
|
||||
self.last_id = self.last_id + 1;
|
||||
|
||||
self.downloads.push(Download::new(self.last_id));
|
||||
}
|
||||
Message::Download(index) => {
|
||||
if let Some(download) = self.downloads.get_mut(index) {
|
||||
download.start();
|
||||
}
|
||||
}
|
||||
Message::DownloadProgressed((id, progress)) => {
|
||||
if let Some(download) =
|
||||
self.downloads.iter_mut().find(|download| download.id == id)
|
||||
{
|
||||
download.progress(progress);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch(self.downloads.iter().map(Download::subscription))
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let downloads = self
|
||||
.downloads
|
||||
.iter_mut()
|
||||
.fold(Column::new().spacing(20), |column, download| {
|
||||
column.push(download.view())
|
||||
})
|
||||
.push(
|
||||
Button::new(&mut self.add, Text::new("Add another download"))
|
||||
.on_press(Message::Add)
|
||||
.padding(10),
|
||||
)
|
||||
.align_items(Align::End);
|
||||
|
||||
Container::new(downloads)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_x()
|
||||
.center_y()
|
||||
.padding(20)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Download {
|
||||
id: usize,
|
||||
state: State,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum State {
|
||||
Idle { button: button::State },
|
||||
Downloading { progress: f32 },
|
||||
Finished { button: button::State },
|
||||
Errored { button: button::State },
|
||||
}
|
||||
|
||||
impl Download {
|
||||
pub fn new(id: usize) -> Self {
|
||||
Download {
|
||||
id,
|
||||
state: State::Idle {
|
||||
button: button::State::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
match self.state {
|
||||
State::Idle { .. }
|
||||
| State::Finished { .. }
|
||||
| State::Errored { .. } => {
|
||||
self.state = State::Downloading { progress: 0.0 };
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn progress(&mut self, new_progress: download::Progress) {
|
||||
match &mut self.state {
|
||||
State::Downloading { progress } => match new_progress {
|
||||
download::Progress::Started => {
|
||||
*progress = 0.0;
|
||||
}
|
||||
download::Progress::Advanced(percentage) => {
|
||||
*progress = percentage;
|
||||
}
|
||||
download::Progress::Finished => {
|
||||
self.state = State::Finished {
|
||||
button: button::State::new(),
|
||||
}
|
||||
}
|
||||
download::Progress::Errored => {
|
||||
self.state = State::Errored {
|
||||
button: button::State::new(),
|
||||
};
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscription(&self) -> Subscription<Message> {
|
||||
match self.state {
|
||||
State::Downloading { .. } => {
|
||||
download::file(self.id, "https://speed.hetzner.de/100MB.bin?")
|
||||
.map(Message::DownloadProgressed)
|
||||
}
|
||||
_ => Subscription::none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(&mut self) -> Element<Message> {
|
||||
let current_progress = match &self.state {
|
||||
State::Idle { .. } => 0.0,
|
||||
State::Downloading { progress } => *progress,
|
||||
State::Finished { .. } => 100.0,
|
||||
State::Errored { .. } => 0.0,
|
||||
};
|
||||
|
||||
let progress_bar = ProgressBar::new(0.0..=100.0, current_progress);
|
||||
|
||||
let control: Element<_> = match &mut self.state {
|
||||
State::Idle { button } => {
|
||||
Button::new(button, Text::new("Start the download!"))
|
||||
.on_press(Message::Download(self.id))
|
||||
.into()
|
||||
}
|
||||
State::Finished { button } => Column::new()
|
||||
.spacing(10)
|
||||
.align_items(Align::Center)
|
||||
.push(Text::new("Download finished!"))
|
||||
.push(
|
||||
Button::new(button, Text::new("Start again"))
|
||||
.on_press(Message::Download(self.id)),
|
||||
)
|
||||
.into(),
|
||||
State::Downloading { .. } => {
|
||||
Text::new(format!("Downloading... {:.2}%", current_progress))
|
||||
.into()
|
||||
}
|
||||
State::Errored { button } => Column::new()
|
||||
.spacing(10)
|
||||
.align_items(Align::Center)
|
||||
.push(Text::new("Something went wrong :("))
|
||||
.push(
|
||||
Button::new(button, Text::new("Try again"))
|
||||
.on_press(Message::Download(self.id)),
|
||||
)
|
||||
.into(),
|
||||
};
|
||||
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.padding(10)
|
||||
.align_items(Align::Center)
|
||||
.push(progress_bar)
|
||||
.push(control)
|
||||
.into()
|
||||
}
|
||||
}
|
10
examples/events/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "events"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../.." }
|
||||
iced_native = { path = "../../native" }
|
18
examples/events/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
## Events
|
||||
|
||||
A log of native events displayed using a conditional `Subscription`.
|
||||
|
||||
The __[`main`]__ file contains all the code of the example.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/infamousicyermine">
|
||||
<img src="https://thumbs.gfycat.com/InfamousIcyErmine-small.gif">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package events
|
||||
```
|
||||
|
||||
[`main`]: src/main.rs
|
118
examples/events/src/main.rs
Normal file
@ -0,0 +1,118 @@
|
||||
use iced::{
|
||||
button, executor, Align, Application, Button, Checkbox, Clipboard, Column,
|
||||
Command, Container, Element, HorizontalAlignment, Length, Settings,
|
||||
Subscription, Text,
|
||||
};
|
||||
use iced_native::{window, Event};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
Events::run(Settings {
|
||||
exit_on_close_request: false,
|
||||
..Settings::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Events {
|
||||
last: Vec<iced_native::Event>,
|
||||
enabled: bool,
|
||||
exit: button::State,
|
||||
should_exit: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
EventOccurred(iced_native::Event),
|
||||
Toggled(bool),
|
||||
Exit,
|
||||
}
|
||||
|
||||
impl Application for Events {
|
||||
type Executor = executor::Default;
|
||||
type Message = Message;
|
||||
type Flags = ();
|
||||
|
||||
fn new(_flags: ()) -> (Events, Command<Message>) {
|
||||
(Events::default(), Command::none())
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Events - Iced")
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Message,
|
||||
_clipboard: &mut Clipboard,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::EventOccurred(event) if self.enabled => {
|
||||
self.last.push(event);
|
||||
|
||||
if self.last.len() > 5 {
|
||||
let _ = self.last.remove(0);
|
||||
}
|
||||
}
|
||||
Message::EventOccurred(event) => {
|
||||
if let Event::Window(window::Event::CloseRequested) = event {
|
||||
self.should_exit = true;
|
||||
}
|
||||
}
|
||||
Message::Toggled(enabled) => {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
Message::Exit => {
|
||||
self.should_exit = true;
|
||||
}
|
||||
};
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
iced_native::subscription::events().map(Message::EventOccurred)
|
||||
}
|
||||
|
||||
fn should_exit(&self) -> bool {
|
||||
self.should_exit
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let events = self.last.iter().fold(
|
||||
Column::new().spacing(10),
|
||||
|column, event| {
|
||||
column.push(Text::new(format!("{:?}", event)).size(40))
|
||||
},
|
||||
);
|
||||
|
||||
let toggle = Checkbox::new(
|
||||
self.enabled,
|
||||
"Listen to runtime events",
|
||||
Message::Toggled,
|
||||
);
|
||||
|
||||
let exit = Button::new(
|
||||
&mut self.exit,
|
||||
Text::new("Exit")
|
||||
.width(Length::Fill)
|
||||
.horizontal_alignment(HorizontalAlignment::Center),
|
||||
)
|
||||
.width(Length::Units(100))
|
||||
.padding(10)
|
||||
.on_press(Message::Exit);
|
||||
|
||||
let content = Column::new()
|
||||
.align_items(Align::Center)
|
||||
.spacing(20)
|
||||
.push(events)
|
||||
.push(toggle)
|
||||
.push(exit);
|
||||
|
||||
Container::new(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_x()
|
||||
.center_y()
|
||||
.into()
|
||||
}
|
||||
}
|
12
examples/game_of_life/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "game_of_life"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
|
||||
tokio = { version = "1.0", features = ["sync"] }
|
||||
itertools = "0.9"
|
||||
rustc-hash = "1.1"
|
22
examples/game_of_life/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
## Game of Life
|
||||
|
||||
An interactive version of the [Game of Life], invented by [John Horton Conway].
|
||||
|
||||
It runs a simulation in a background thread while allowing interaction with a `Canvas` that displays an infinite grid with zooming, panning, and drawing support.
|
||||
|
||||
The __[`main`]__ file contains the relevant code of the example.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/WhichPaltryChick">
|
||||
<img src="https://thumbs.gfycat.com/WhichPaltryChick-size_restricted.gif">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package game_of_life
|
||||
```
|
||||
|
||||
[`main`]: src/main.rs
|
||||
[Game of Life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
|
||||
[John Horton Conway]: https://en.wikipedia.org/wiki/John_Horton_Conway
|
889
examples/game_of_life/src/main.rs
Normal file
@ -0,0 +1,889 @@
|
||||
//! This example showcases an interactive version of the Game of Life, invented
|
||||
//! by John Conway. It leverages a `Canvas` together with other widgets.
|
||||
mod preset;
|
||||
mod style;
|
||||
|
||||
use grid::Grid;
|
||||
use iced::button::{self, Button};
|
||||
use iced::executor;
|
||||
use iced::pick_list::{self, PickList};
|
||||
use iced::slider::{self, Slider};
|
||||
use iced::time;
|
||||
use iced::{
|
||||
Align, Application, Checkbox, Clipboard, Column, Command, Container,
|
||||
Element, Length, Row, Settings, Subscription, Text,
|
||||
};
|
||||
use preset::Preset;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
GameOfLife::run(Settings {
|
||||
antialiasing: true,
|
||||
..Settings::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GameOfLife {
|
||||
grid: Grid,
|
||||
controls: Controls,
|
||||
is_playing: bool,
|
||||
queued_ticks: usize,
|
||||
speed: usize,
|
||||
next_speed: Option<usize>,
|
||||
version: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
Grid(grid::Message, usize),
|
||||
Tick(Instant),
|
||||
TogglePlayback,
|
||||
ToggleGrid(bool),
|
||||
Next,
|
||||
Clear,
|
||||
SpeedChanged(f32),
|
||||
PresetPicked(Preset),
|
||||
}
|
||||
|
||||
impl Application for GameOfLife {
|
||||
type Message = Message;
|
||||
type Executor = executor::Default;
|
||||
type Flags = ();
|
||||
|
||||
fn new(_flags: ()) -> (Self, Command<Message>) {
|
||||
(
|
||||
Self {
|
||||
speed: 5,
|
||||
..Self::default()
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Game of Life - Iced")
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Message,
|
||||
_clipboard: &mut Clipboard,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::Grid(message, version) => {
|
||||
if version == self.version {
|
||||
self.grid.update(message);
|
||||
}
|
||||
}
|
||||
Message::Tick(_) | Message::Next => {
|
||||
self.queued_ticks = (self.queued_ticks + 1).min(self.speed);
|
||||
|
||||
if let Some(task) = self.grid.tick(self.queued_ticks) {
|
||||
if let Some(speed) = self.next_speed.take() {
|
||||
self.speed = speed;
|
||||
}
|
||||
|
||||
self.queued_ticks = 0;
|
||||
|
||||
let version = self.version;
|
||||
|
||||
return Command::perform(task, move |message| {
|
||||
Message::Grid(message, version)
|
||||
});
|
||||
}
|
||||
}
|
||||
Message::TogglePlayback => {
|
||||
self.is_playing = !self.is_playing;
|
||||
}
|
||||
Message::ToggleGrid(show_grid_lines) => {
|
||||
self.grid.toggle_lines(show_grid_lines);
|
||||
}
|
||||
Message::Clear => {
|
||||
self.grid.clear();
|
||||
self.version += 1;
|
||||
}
|
||||
Message::SpeedChanged(speed) => {
|
||||
if self.is_playing {
|
||||
self.next_speed = Some(speed.round() as usize);
|
||||
} else {
|
||||
self.speed = speed.round() as usize;
|
||||
}
|
||||
}
|
||||
Message::PresetPicked(new_preset) => {
|
||||
self.grid = Grid::from_preset(new_preset);
|
||||
self.version += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
if self.is_playing {
|
||||
time::every(Duration::from_millis(1000 / self.speed as u64))
|
||||
.map(Message::Tick)
|
||||
} else {
|
||||
Subscription::none()
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let version = self.version;
|
||||
let selected_speed = self.next_speed.unwrap_or(self.speed);
|
||||
let controls = self.controls.view(
|
||||
self.is_playing,
|
||||
self.grid.are_lines_visible(),
|
||||
selected_speed,
|
||||
self.grid.preset(),
|
||||
);
|
||||
|
||||
let content = Column::new()
|
||||
.push(
|
||||
self.grid
|
||||
.view()
|
||||
.map(move |message| Message::Grid(message, version)),
|
||||
)
|
||||
.push(controls);
|
||||
|
||||
Container::new(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(style::Container)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
mod grid {
|
||||
use crate::Preset;
|
||||
use iced::{
|
||||
canvas::event::{self, Event},
|
||||
canvas::{self, Cache, Canvas, Cursor, Frame, Geometry, Path, Text},
|
||||
mouse, Color, Element, HorizontalAlignment, Length, Point, Rectangle,
|
||||
Size, Vector, VerticalAlignment,
|
||||
};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use std::future::Future;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub struct Grid {
|
||||
state: State,
|
||||
preset: Preset,
|
||||
interaction: Interaction,
|
||||
life_cache: Cache,
|
||||
grid_cache: Cache,
|
||||
translation: Vector,
|
||||
scaling: f32,
|
||||
show_lines: bool,
|
||||
last_tick_duration: Duration,
|
||||
last_queued_ticks: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
Populate(Cell),
|
||||
Unpopulate(Cell),
|
||||
Ticked {
|
||||
result: Result<Life, TickError>,
|
||||
tick_duration: Duration,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TickError {
|
||||
JoinFailed,
|
||||
}
|
||||
|
||||
impl Default for Grid {
|
||||
fn default() -> Self {
|
||||
Self::from_preset(Preset::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Grid {
|
||||
const MIN_SCALING: f32 = 0.1;
|
||||
const MAX_SCALING: f32 = 2.0;
|
||||
|
||||
pub fn from_preset(preset: Preset) -> Self {
|
||||
Self {
|
||||
state: State::with_life(
|
||||
preset
|
||||
.life()
|
||||
.into_iter()
|
||||
.map(|(i, j)| Cell { i, j })
|
||||
.collect(),
|
||||
),
|
||||
preset,
|
||||
interaction: Interaction::None,
|
||||
life_cache: Cache::default(),
|
||||
grid_cache: Cache::default(),
|
||||
translation: Vector::default(),
|
||||
scaling: 1.0,
|
||||
show_lines: true,
|
||||
last_tick_duration: Duration::default(),
|
||||
last_queued_ticks: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(
|
||||
&mut self,
|
||||
amount: usize,
|
||||
) -> Option<impl Future<Output = Message>> {
|
||||
let tick = self.state.tick(amount)?;
|
||||
|
||||
self.last_queued_ticks = amount;
|
||||
|
||||
Some(async move {
|
||||
let start = Instant::now();
|
||||
let result = tick.await;
|
||||
let tick_duration = start.elapsed() / amount as u32;
|
||||
|
||||
Message::Ticked {
|
||||
result,
|
||||
tick_duration,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::Populate(cell) => {
|
||||
self.state.populate(cell);
|
||||
self.life_cache.clear();
|
||||
|
||||
self.preset = Preset::Custom;
|
||||
}
|
||||
Message::Unpopulate(cell) => {
|
||||
self.state.unpopulate(&cell);
|
||||
self.life_cache.clear();
|
||||
|
||||
self.preset = Preset::Custom;
|
||||
}
|
||||
Message::Ticked {
|
||||
result: Ok(life),
|
||||
tick_duration,
|
||||
} => {
|
||||
self.state.update(life);
|
||||
self.life_cache.clear();
|
||||
|
||||
self.last_tick_duration = tick_duration;
|
||||
}
|
||||
Message::Ticked {
|
||||
result: Err(error), ..
|
||||
} => {
|
||||
dbg!(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view<'a>(&'a mut self) -> Element<'a, Message> {
|
||||
Canvas::new(self)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.state = State::default();
|
||||
self.preset = Preset::Custom;
|
||||
|
||||
self.life_cache.clear();
|
||||
}
|
||||
|
||||
pub fn preset(&self) -> Preset {
|
||||
self.preset
|
||||
}
|
||||
|
||||
pub fn toggle_lines(&mut self, enabled: bool) {
|
||||
self.show_lines = enabled;
|
||||
}
|
||||
|
||||
pub fn are_lines_visible(&self) -> bool {
|
||||
self.show_lines
|
||||
}
|
||||
|
||||
fn visible_region(&self, size: Size) -> Region {
|
||||
let width = size.width / self.scaling;
|
||||
let height = size.height / self.scaling;
|
||||
|
||||
Region {
|
||||
x: -self.translation.x - width / 2.0,
|
||||
y: -self.translation.y - height / 2.0,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
fn project(&self, position: Point, size: Size) -> Point {
|
||||
let region = self.visible_region(size);
|
||||
|
||||
Point::new(
|
||||
position.x / self.scaling + region.x,
|
||||
position.y / self.scaling + region.y,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> canvas::Program<Message> for Grid {
|
||||
fn update(
|
||||
&mut self,
|
||||
event: Event,
|
||||
bounds: Rectangle,
|
||||
cursor: Cursor,
|
||||
) -> (event::Status, Option<Message>) {
|
||||
if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event {
|
||||
self.interaction = Interaction::None;
|
||||
}
|
||||
|
||||
let cursor_position =
|
||||
if let Some(position) = cursor.position_in(&bounds) {
|
||||
position
|
||||
} else {
|
||||
return (event::Status::Ignored, None);
|
||||
};
|
||||
|
||||
let cell = Cell::at(self.project(cursor_position, bounds.size()));
|
||||
let is_populated = self.state.contains(&cell);
|
||||
|
||||
let (populate, unpopulate) = if is_populated {
|
||||
(None, Some(Message::Unpopulate(cell)))
|
||||
} else {
|
||||
(Some(Message::Populate(cell)), None)
|
||||
};
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse_event) => match mouse_event {
|
||||
mouse::Event::ButtonPressed(button) => {
|
||||
let message = match button {
|
||||
mouse::Button::Left => {
|
||||
self.interaction = if is_populated {
|
||||
Interaction::Erasing
|
||||
} else {
|
||||
Interaction::Drawing
|
||||
};
|
||||
|
||||
populate.or(unpopulate)
|
||||
}
|
||||
mouse::Button::Right => {
|
||||
self.interaction = Interaction::Panning {
|
||||
translation: self.translation,
|
||||
start: cursor_position,
|
||||
};
|
||||
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
(event::Status::Captured, message)
|
||||
}
|
||||
mouse::Event::CursorMoved { .. } => {
|
||||
let message = match self.interaction {
|
||||
Interaction::Drawing => populate,
|
||||
Interaction::Erasing => unpopulate,
|
||||
Interaction::Panning { translation, start } => {
|
||||
self.translation = translation
|
||||
+ (cursor_position - start)
|
||||
* (1.0 / self.scaling);
|
||||
|
||||
self.life_cache.clear();
|
||||
self.grid_cache.clear();
|
||||
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let event_status = match self.interaction {
|
||||
Interaction::None => event::Status::Ignored,
|
||||
_ => event::Status::Captured,
|
||||
};
|
||||
|
||||
(event_status, message)
|
||||
}
|
||||
mouse::Event::WheelScrolled { delta } => match delta {
|
||||
mouse::ScrollDelta::Lines { y, .. }
|
||||
| mouse::ScrollDelta::Pixels { y, .. } => {
|
||||
if y < 0.0 && self.scaling > Self::MIN_SCALING
|
||||
|| y > 0.0 && self.scaling < Self::MAX_SCALING
|
||||
{
|
||||
let old_scaling = self.scaling;
|
||||
|
||||
self.scaling = (self.scaling
|
||||
* (1.0 + y / 30.0))
|
||||
.max(Self::MIN_SCALING)
|
||||
.min(Self::MAX_SCALING);
|
||||
|
||||
if let Some(cursor_to_center) =
|
||||
cursor.position_from(bounds.center())
|
||||
{
|
||||
let factor = self.scaling - old_scaling;
|
||||
|
||||
self.translation = self.translation
|
||||
- Vector::new(
|
||||
cursor_to_center.x * factor
|
||||
/ (old_scaling * old_scaling),
|
||||
cursor_to_center.y * factor
|
||||
/ (old_scaling * old_scaling),
|
||||
);
|
||||
}
|
||||
|
||||
self.life_cache.clear();
|
||||
self.grid_cache.clear();
|
||||
}
|
||||
|
||||
(event::Status::Captured, None)
|
||||
}
|
||||
},
|
||||
_ => (event::Status::Ignored, None),
|
||||
},
|
||||
_ => (event::Status::Ignored, None),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> {
|
||||
let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0);
|
||||
|
||||
let life = self.life_cache.draw(bounds.size(), |frame| {
|
||||
let background = Path::rectangle(Point::ORIGIN, frame.size());
|
||||
frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B));
|
||||
|
||||
frame.with_save(|frame| {
|
||||
frame.translate(center);
|
||||
frame.scale(self.scaling);
|
||||
frame.translate(self.translation);
|
||||
frame.scale(Cell::SIZE as f32);
|
||||
|
||||
let region = self.visible_region(frame.size());
|
||||
|
||||
for cell in region.cull(self.state.cells()) {
|
||||
frame.fill_rectangle(
|
||||
Point::new(cell.j as f32, cell.i as f32),
|
||||
Size::UNIT,
|
||||
Color::WHITE,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let overlay = {
|
||||
let mut frame = Frame::new(bounds.size());
|
||||
|
||||
let hovered_cell =
|
||||
cursor.position_in(&bounds).map(|position| {
|
||||
Cell::at(self.project(position, frame.size()))
|
||||
});
|
||||
|
||||
if let Some(cell) = hovered_cell {
|
||||
frame.with_save(|frame| {
|
||||
frame.translate(center);
|
||||
frame.scale(self.scaling);
|
||||
frame.translate(self.translation);
|
||||
frame.scale(Cell::SIZE as f32);
|
||||
|
||||
frame.fill_rectangle(
|
||||
Point::new(cell.j as f32, cell.i as f32),
|
||||
Size::UNIT,
|
||||
Color {
|
||||
a: 0.5,
|
||||
..Color::BLACK
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let text = Text {
|
||||
color: Color::WHITE,
|
||||
size: 14.0,
|
||||
position: Point::new(frame.width(), frame.height()),
|
||||
horizontal_alignment: HorizontalAlignment::Right,
|
||||
vertical_alignment: VerticalAlignment::Bottom,
|
||||
..Text::default()
|
||||
};
|
||||
|
||||
if let Some(cell) = hovered_cell {
|
||||
frame.fill_text(Text {
|
||||
content: format!("({}, {})", cell.j, cell.i),
|
||||
position: text.position - Vector::new(0.0, 16.0),
|
||||
..text
|
||||
});
|
||||
}
|
||||
|
||||
let cell_count = self.state.cell_count();
|
||||
|
||||
frame.fill_text(Text {
|
||||
content: format!(
|
||||
"{} cell{} @ {:?} ({})",
|
||||
cell_count,
|
||||
if cell_count == 1 { "" } else { "s" },
|
||||
self.last_tick_duration,
|
||||
self.last_queued_ticks
|
||||
),
|
||||
..text
|
||||
});
|
||||
|
||||
frame.into_geometry()
|
||||
};
|
||||
|
||||
if self.scaling < 0.2 || !self.show_lines {
|
||||
vec![life, overlay]
|
||||
} else {
|
||||
let grid = self.grid_cache.draw(bounds.size(), |frame| {
|
||||
frame.translate(center);
|
||||
frame.scale(self.scaling);
|
||||
frame.translate(self.translation);
|
||||
frame.scale(Cell::SIZE as f32);
|
||||
|
||||
let region = self.visible_region(frame.size());
|
||||
let rows = region.rows();
|
||||
let columns = region.columns();
|
||||
let (total_rows, total_columns) =
|
||||
(rows.clone().count(), columns.clone().count());
|
||||
let width = 2.0 / Cell::SIZE as f32;
|
||||
let color = Color::from_rgb8(70, 74, 83);
|
||||
|
||||
frame.translate(Vector::new(-width / 2.0, -width / 2.0));
|
||||
|
||||
for row in region.rows() {
|
||||
frame.fill_rectangle(
|
||||
Point::new(*columns.start() as f32, row as f32),
|
||||
Size::new(total_columns as f32, width),
|
||||
color,
|
||||
);
|
||||
}
|
||||
|
||||
for column in region.columns() {
|
||||
frame.fill_rectangle(
|
||||
Point::new(column as f32, *rows.start() as f32),
|
||||
Size::new(width, total_rows as f32),
|
||||
color,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
vec![life, grid, overlay]
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
bounds: Rectangle,
|
||||
cursor: Cursor,
|
||||
) -> mouse::Interaction {
|
||||
match self.interaction {
|
||||
Interaction::Drawing => mouse::Interaction::Crosshair,
|
||||
Interaction::Erasing => mouse::Interaction::Crosshair,
|
||||
Interaction::Panning { .. } => mouse::Interaction::Grabbing,
|
||||
Interaction::None if cursor.is_over(&bounds) => {
|
||||
mouse::Interaction::Crosshair
|
||||
}
|
||||
_ => mouse::Interaction::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
life: Life,
|
||||
births: FxHashSet<Cell>,
|
||||
is_ticking: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn with_life(life: Life) -> Self {
|
||||
Self {
|
||||
life,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn cell_count(&self) -> usize {
|
||||
self.life.len() + self.births.len()
|
||||
}
|
||||
|
||||
fn contains(&self, cell: &Cell) -> bool {
|
||||
self.life.contains(cell) || self.births.contains(cell)
|
||||
}
|
||||
|
||||
fn cells(&self) -> impl Iterator<Item = &Cell> {
|
||||
self.life.iter().chain(self.births.iter())
|
||||
}
|
||||
|
||||
fn populate(&mut self, cell: Cell) {
|
||||
if self.is_ticking {
|
||||
self.births.insert(cell);
|
||||
} else {
|
||||
self.life.populate(cell);
|
||||
}
|
||||
}
|
||||
|
||||
fn unpopulate(&mut self, cell: &Cell) {
|
||||
if self.is_ticking {
|
||||
let _ = self.births.remove(cell);
|
||||
} else {
|
||||
self.life.unpopulate(cell);
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, mut life: Life) {
|
||||
self.births.drain().for_each(|cell| life.populate(cell));
|
||||
|
||||
self.life = life;
|
||||
self.is_ticking = false;
|
||||
}
|
||||
|
||||
fn tick(
|
||||
&mut self,
|
||||
amount: usize,
|
||||
) -> Option<impl Future<Output = Result<Life, TickError>>> {
|
||||
if self.is_ticking {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.is_ticking = true;
|
||||
|
||||
let mut life = self.life.clone();
|
||||
|
||||
Some(async move {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
for _ in 0..amount {
|
||||
life.tick();
|
||||
}
|
||||
|
||||
life
|
||||
})
|
||||
.await
|
||||
.map_err(|_| TickError::JoinFailed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Life {
|
||||
cells: FxHashSet<Cell>,
|
||||
}
|
||||
|
||||
impl Life {
|
||||
fn len(&self) -> usize {
|
||||
self.cells.len()
|
||||
}
|
||||
|
||||
fn contains(&self, cell: &Cell) -> bool {
|
||||
self.cells.contains(cell)
|
||||
}
|
||||
|
||||
fn populate(&mut self, cell: Cell) {
|
||||
self.cells.insert(cell);
|
||||
}
|
||||
|
||||
fn unpopulate(&mut self, cell: &Cell) {
|
||||
let _ = self.cells.remove(cell);
|
||||
}
|
||||
|
||||
fn tick(&mut self) {
|
||||
let mut adjacent_life = FxHashMap::default();
|
||||
|
||||
for cell in &self.cells {
|
||||
let _ = adjacent_life.entry(*cell).or_insert(0);
|
||||
|
||||
for neighbor in Cell::neighbors(*cell) {
|
||||
let amount = adjacent_life.entry(neighbor).or_insert(0);
|
||||
|
||||
*amount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (cell, amount) in adjacent_life.iter() {
|
||||
match amount {
|
||||
2 => {}
|
||||
3 => {
|
||||
let _ = self.cells.insert(*cell);
|
||||
}
|
||||
_ => {
|
||||
let _ = self.cells.remove(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &Cell> {
|
||||
self.cells.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FromIterator<Cell> for Life {
|
||||
fn from_iter<I: IntoIterator<Item = Cell>>(iter: I) -> Self {
|
||||
Life {
|
||||
cells: iter.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Life {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Life")
|
||||
.field("cells", &self.cells.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Cell {
|
||||
i: isize,
|
||||
j: isize,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
const SIZE: usize = 20;
|
||||
|
||||
fn at(position: Point) -> Cell {
|
||||
let i = (position.y / Cell::SIZE as f32).ceil() as isize;
|
||||
let j = (position.x / Cell::SIZE as f32).ceil() as isize;
|
||||
|
||||
Cell {
|
||||
i: i.saturating_sub(1),
|
||||
j: j.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
fn cluster(cell: Cell) -> impl Iterator<Item = Cell> {
|
||||
use itertools::Itertools;
|
||||
|
||||
let rows = cell.i.saturating_sub(1)..=cell.i.saturating_add(1);
|
||||
let columns = cell.j.saturating_sub(1)..=cell.j.saturating_add(1);
|
||||
|
||||
rows.cartesian_product(columns).map(|(i, j)| Cell { i, j })
|
||||
}
|
||||
|
||||
fn neighbors(cell: Cell) -> impl Iterator<Item = Cell> {
|
||||
Cell::cluster(cell).filter(move |candidate| *candidate != cell)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Region {
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
}
|
||||
|
||||
impl Region {
|
||||
fn rows(&self) -> RangeInclusive<isize> {
|
||||
let first_row = (self.y / Cell::SIZE as f32).floor() as isize;
|
||||
|
||||
let visible_rows =
|
||||
(self.height / Cell::SIZE as f32).ceil() as isize;
|
||||
|
||||
first_row..=first_row + visible_rows
|
||||
}
|
||||
|
||||
fn columns(&self) -> RangeInclusive<isize> {
|
||||
let first_column = (self.x / Cell::SIZE as f32).floor() as isize;
|
||||
|
||||
let visible_columns =
|
||||
(self.width / Cell::SIZE as f32).ceil() as isize;
|
||||
|
||||
first_column..=first_column + visible_columns
|
||||
}
|
||||
|
||||
fn cull<'a>(
|
||||
&self,
|
||||
cells: impl Iterator<Item = &'a Cell>,
|
||||
) -> impl Iterator<Item = &'a Cell> {
|
||||
let rows = self.rows();
|
||||
let columns = self.columns();
|
||||
|
||||
cells.filter(move |cell| {
|
||||
rows.contains(&cell.i) && columns.contains(&cell.j)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
enum Interaction {
|
||||
None,
|
||||
Drawing,
|
||||
Erasing,
|
||||
Panning { translation: Vector, start: Point },
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Controls {
|
||||
toggle_button: button::State,
|
||||
next_button: button::State,
|
||||
clear_button: button::State,
|
||||
speed_slider: slider::State,
|
||||
preset_list: pick_list::State<Preset>,
|
||||
}
|
||||
|
||||
impl Controls {
|
||||
fn view<'a>(
|
||||
&'a mut self,
|
||||
is_playing: bool,
|
||||
is_grid_enabled: bool,
|
||||
speed: usize,
|
||||
preset: Preset,
|
||||
) -> Element<'a, Message> {
|
||||
let playback_controls = Row::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
Button::new(
|
||||
&mut self.toggle_button,
|
||||
Text::new(if is_playing { "Pause" } else { "Play" }),
|
||||
)
|
||||
.on_press(Message::TogglePlayback)
|
||||
.style(style::Button),
|
||||
)
|
||||
.push(
|
||||
Button::new(&mut self.next_button, Text::new("Next"))
|
||||
.on_press(Message::Next)
|
||||
.style(style::Button),
|
||||
);
|
||||
|
||||
let speed_controls = Row::new()
|
||||
.width(Length::Fill)
|
||||
.align_items(Align::Center)
|
||||
.spacing(10)
|
||||
.push(
|
||||
Slider::new(
|
||||
&mut self.speed_slider,
|
||||
1.0..=1000.0,
|
||||
speed as f32,
|
||||
Message::SpeedChanged,
|
||||
)
|
||||
.style(style::Slider),
|
||||
)
|
||||
.push(Text::new(format!("x{}", speed)).size(16));
|
||||
|
||||
Row::new()
|
||||
.padding(10)
|
||||
.spacing(20)
|
||||
.align_items(Align::Center)
|
||||
.push(playback_controls)
|
||||
.push(speed_controls)
|
||||
.push(
|
||||
Checkbox::new(is_grid_enabled, "Grid", Message::ToggleGrid)
|
||||
.size(16)
|
||||
.spacing(5)
|
||||
.text_size(16),
|
||||
)
|
||||
.push(
|
||||
PickList::new(
|
||||
&mut self.preset_list,
|
||||
preset::ALL,
|
||||
Some(preset),
|
||||
Message::PresetPicked,
|
||||
)
|
||||
.padding(8)
|
||||
.text_size(16)
|
||||
.style(style::PickList),
|
||||
)
|
||||
.push(
|
||||
Button::new(&mut self.clear_button, Text::new("Clear"))
|
||||
.on_press(Message::Clear)
|
||||
.style(style::Clear),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
142
examples/game_of_life/src/preset.rs
Normal file
@ -0,0 +1,142 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Preset {
|
||||
Custom,
|
||||
XKCD,
|
||||
Glider,
|
||||
SmallExploder,
|
||||
Exploder,
|
||||
TenCellRow,
|
||||
LightweightSpaceship,
|
||||
Tumbler,
|
||||
GliderGun,
|
||||
Acorn,
|
||||
}
|
||||
|
||||
pub static ALL: &[Preset] = &[
|
||||
Preset::Custom,
|
||||
Preset::XKCD,
|
||||
Preset::Glider,
|
||||
Preset::SmallExploder,
|
||||
Preset::Exploder,
|
||||
Preset::TenCellRow,
|
||||
Preset::LightweightSpaceship,
|
||||
Preset::Tumbler,
|
||||
Preset::GliderGun,
|
||||
Preset::Acorn,
|
||||
];
|
||||
|
||||
impl Preset {
|
||||
pub fn life(self) -> Vec<(isize, isize)> {
|
||||
#[rustfmt::skip]
|
||||
let cells = match self {
|
||||
Preset::Custom => vec![],
|
||||
Preset::XKCD => vec![
|
||||
" xxx ",
|
||||
" x x ",
|
||||
" x x ",
|
||||
" x ",
|
||||
"x xxx ",
|
||||
" x x x ",
|
||||
" x x",
|
||||
" x x ",
|
||||
" x x ",
|
||||
],
|
||||
Preset::Glider => vec![
|
||||
" x ",
|
||||
" x",
|
||||
"xxx"
|
||||
],
|
||||
Preset::SmallExploder => vec![
|
||||
" x ",
|
||||
"xxx",
|
||||
"x x",
|
||||
" x ",
|
||||
],
|
||||
Preset::Exploder => vec![
|
||||
"x x x",
|
||||
"x x",
|
||||
"x x",
|
||||
"x x",
|
||||
"x x x",
|
||||
],
|
||||
Preset::TenCellRow => vec![
|
||||
"xxxxxxxxxx",
|
||||
],
|
||||
Preset::LightweightSpaceship => vec![
|
||||
" xxxxx",
|
||||
"x x",
|
||||
" x",
|
||||
"x x ",
|
||||
],
|
||||
Preset::Tumbler => vec![
|
||||
" xx xx ",
|
||||
" xx xx ",
|
||||
" x x ",
|
||||
"x x x x",
|
||||
"x x x x",
|
||||
"xx xx",
|
||||
],
|
||||
Preset::GliderGun => vec![
|
||||
" x ",
|
||||
" x x ",
|
||||
" xx xx xx",
|
||||
" x x xx xx",
|
||||
"xx x x xx ",
|
||||
"xx x x xx x x ",
|
||||
" x x x ",
|
||||
" x x ",
|
||||
" xx ",
|
||||
],
|
||||
Preset::Acorn => vec![
|
||||
" x ",
|
||||
" x ",
|
||||
"xx xxx",
|
||||
],
|
||||
};
|
||||
|
||||
let start_row = -(cells.len() as isize / 2);
|
||||
|
||||
cells
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.flat_map(|(i, cells)| {
|
||||
let start_column = -(cells.len() as isize / 2);
|
||||
|
||||
cells
|
||||
.chars()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| !c.is_whitespace())
|
||||
.map(move |(j, _)| {
|
||||
(start_row + i as isize, start_column + j as isize)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Preset {
|
||||
fn default() -> Preset {
|
||||
Preset::XKCD
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Preset {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Preset::Custom => "Custom",
|
||||
Preset::XKCD => "xkcd #2293",
|
||||
Preset::Glider => "Glider",
|
||||
Preset::SmallExploder => "Small Exploder",
|
||||
Preset::Exploder => "Exploder",
|
||||
Preset::TenCellRow => "10 Cell Row",
|
||||
Preset::LightweightSpaceship => "Lightweight spaceship",
|
||||
Preset::Tumbler => "Tumbler",
|
||||
Preset::GliderGun => "Gosper Glider Gun",
|
||||
Preset::Acorn => "Acorn",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
188
examples/game_of_life/src/style.rs
Normal file
@ -0,0 +1,188 @@
|
||||
use iced::{button, container, pick_list, slider, Background, Color};
|
||||
|
||||
const ACTIVE: Color = Color::from_rgb(
|
||||
0x72 as f32 / 255.0,
|
||||
0x89 as f32 / 255.0,
|
||||
0xDA as f32 / 255.0,
|
||||
);
|
||||
|
||||
const DESTRUCTIVE: Color = Color::from_rgb(
|
||||
0xC0 as f32 / 255.0,
|
||||
0x47 as f32 / 255.0,
|
||||
0x47 as f32 / 255.0,
|
||||
);
|
||||
|
||||
const HOVERED: Color = Color::from_rgb(
|
||||
0x67 as f32 / 255.0,
|
||||
0x7B as f32 / 255.0,
|
||||
0xC4 as f32 / 255.0,
|
||||
);
|
||||
|
||||
const BACKGROUND: Color = Color::from_rgb(
|
||||
0x2F as f32 / 255.0,
|
||||
0x31 as f32 / 255.0,
|
||||
0x36 as f32 / 255.0,
|
||||
);
|
||||
|
||||
pub struct Container;
|
||||
|
||||
impl container::StyleSheet for Container {
|
||||
fn style(&self) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(Color::from_rgb8(
|
||||
0x36, 0x39, 0x3F,
|
||||
))),
|
||||
text_color: Some(Color::WHITE),
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Button;
|
||||
|
||||
impl button::StyleSheet for Button {
|
||||
fn active(&self) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(ACTIVE)),
|
||||
border_radius: 3.0,
|
||||
text_color: Color::WHITE,
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(HOVERED)),
|
||||
text_color: Color::WHITE,
|
||||
..self.active()
|
||||
}
|
||||
}
|
||||
|
||||
fn pressed(&self) -> button::Style {
|
||||
button::Style {
|
||||
border_width: 1.0,
|
||||
border_color: Color::WHITE,
|
||||
..self.hovered()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Clear;
|
||||
|
||||
impl button::StyleSheet for Clear {
|
||||
fn active(&self) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(DESTRUCTIVE)),
|
||||
border_radius: 3.0,
|
||||
text_color: Color::WHITE,
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.5,
|
||||
..DESTRUCTIVE
|
||||
})),
|
||||
text_color: Color::WHITE,
|
||||
..self.active()
|
||||
}
|
||||
}
|
||||
|
||||
fn pressed(&self) -> button::Style {
|
||||
button::Style {
|
||||
border_width: 1.0,
|
||||
border_color: Color::WHITE,
|
||||
..self.hovered()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Slider;
|
||||
|
||||
impl slider::StyleSheet for Slider {
|
||||
fn active(&self) -> slider::Style {
|
||||
slider::Style {
|
||||
rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }),
|
||||
handle: slider::Handle {
|
||||
shape: slider::HandleShape::Circle { radius: 9.0 },
|
||||
color: ACTIVE,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self) -> slider::Style {
|
||||
let active = self.active();
|
||||
|
||||
slider::Style {
|
||||
handle: slider::Handle {
|
||||
color: HOVERED,
|
||||
..active.handle
|
||||
},
|
||||
..active
|
||||
}
|
||||
}
|
||||
|
||||
fn dragging(&self) -> slider::Style {
|
||||
let active = self.active();
|
||||
|
||||
slider::Style {
|
||||
handle: slider::Handle {
|
||||
color: Color::from_rgb(0.85, 0.85, 0.85),
|
||||
..active.handle
|
||||
},
|
||||
..active
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PickList;
|
||||
|
||||
impl pick_list::StyleSheet for PickList {
|
||||
fn menu(&self) -> pick_list::Menu {
|
||||
pick_list::Menu {
|
||||
text_color: Color::WHITE,
|
||||
background: BACKGROUND.into(),
|
||||
border_width: 1.0,
|
||||
border_color: Color {
|
||||
a: 0.7,
|
||||
..Color::BLACK
|
||||
},
|
||||
selected_background: Color {
|
||||
a: 0.5,
|
||||
..Color::BLACK
|
||||
}
|
||||
.into(),
|
||||
selected_text_color: Color::WHITE,
|
||||
}
|
||||
}
|
||||
|
||||
fn active(&self) -> pick_list::Style {
|
||||
pick_list::Style {
|
||||
text_color: Color::WHITE,
|
||||
background: BACKGROUND.into(),
|
||||
border_width: 1.0,
|
||||
border_color: Color {
|
||||
a: 0.6,
|
||||
..Color::BLACK
|
||||
},
|
||||
border_radius: 2.0,
|
||||
icon_size: 0.5,
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self) -> pick_list::Style {
|
||||
let active = self.active();
|
||||
|
||||
pick_list::Style {
|
||||
border_color: Color {
|
||||
a: 0.9,
|
||||
..Color::BLACK
|
||||
},
|
||||
..active
|
||||
}
|
||||
}
|
||||
}
|
11
examples/geometry/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "geometry"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../.." }
|
||||
iced_native = { path = "../../native" }
|
||||
iced_graphics = { path = "../../graphics" }
|
18
examples/geometry/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
## Geometry
|
||||
|
||||
A custom widget showcasing how to draw geometry with the `Mesh2D` primitive in [`iced_wgpu`](../../wgpu).
|
||||
|
||||
The __[`main`]__ file contains all the code of the example.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/activeunfitkangaroo">
|
||||
<img src="https://thumbs.gfycat.com/ActiveUnfitKangaroo-small.gif">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package geometry
|
||||
```
|
||||
|
||||
[`main`]: src/main.rs
|
223
examples/geometry/src/main.rs
Normal file
@ -0,0 +1,223 @@
|
||||
//! This example showcases a simple native custom widget that renders using
|
||||
//! arbitrary low-level geometry.
|
||||
mod rainbow {
|
||||
// For now, to implement a custom native widget you will need to add
|
||||
// `iced_native` and `iced_wgpu` to your dependencies.
|
||||
//
|
||||
// Then, you simply need to define your widget type and implement the
|
||||
// `iced_native::Widget` trait with the `iced_wgpu::Renderer`.
|
||||
//
|
||||
// Of course, you can choose to make the implementation renderer-agnostic,
|
||||
// if you wish to, by creating your own `Renderer` trait, which could be
|
||||
// implemented by `iced_wgpu` and other renderers.
|
||||
use iced_graphics::{
|
||||
triangle::{Mesh2D, Vertex2D},
|
||||
Backend, Defaults, Primitive, Renderer,
|
||||
};
|
||||
use iced_native::{
|
||||
layout, mouse, Element, Hasher, Layout, Length, Point, Rectangle, Size,
|
||||
Vector, Widget,
|
||||
};
|
||||
|
||||
pub struct Rainbow;
|
||||
|
||||
impl Rainbow {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, B> Widget<Message, Renderer<B>> for Rainbow
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
Length::Fill
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer<B>,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let size = limits.width(Length::Fill).resolve(Size::ZERO);
|
||||
|
||||
layout::Node::new(Size::new(size.width, size.width))
|
||||
}
|
||||
|
||||
fn hash_layout(&self, _state: &mut Hasher) {}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_renderer: &mut Renderer<B>,
|
||||
_defaults: &Defaults,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) -> (Primitive, mouse::Interaction) {
|
||||
let b = layout.bounds();
|
||||
|
||||
// R O Y G B I V
|
||||
let color_r = [1.0, 0.0, 0.0, 1.0];
|
||||
let color_o = [1.0, 0.5, 0.0, 1.0];
|
||||
let color_y = [1.0, 1.0, 0.0, 1.0];
|
||||
let color_g = [0.0, 1.0, 0.0, 1.0];
|
||||
let color_gb = [0.0, 1.0, 0.5, 1.0];
|
||||
let color_b = [0.0, 0.2, 1.0, 1.0];
|
||||
let color_i = [0.5, 0.0, 1.0, 1.0];
|
||||
let color_v = [0.75, 0.0, 0.5, 1.0];
|
||||
|
||||
let posn_center = {
|
||||
if b.contains(cursor_position) {
|
||||
[cursor_position.x - b.x, cursor_position.y - b.y]
|
||||
} else {
|
||||
[b.width / 2.0, b.height / 2.0]
|
||||
}
|
||||
};
|
||||
|
||||
let posn_tl = [0.0, 0.0];
|
||||
let posn_t = [b.width / 2.0, 0.0];
|
||||
let posn_tr = [b.width, 0.0];
|
||||
let posn_r = [b.width, b.height / 2.0];
|
||||
let posn_br = [b.width, b.height];
|
||||
let posn_b = [(b.width / 2.0), b.height];
|
||||
let posn_bl = [0.0, b.height];
|
||||
let posn_l = [0.0, b.height / 2.0];
|
||||
|
||||
(
|
||||
Primitive::Translate {
|
||||
translation: Vector::new(b.x, b.y),
|
||||
content: Box::new(Primitive::Mesh2D {
|
||||
size: b.size(),
|
||||
buffers: Mesh2D {
|
||||
vertices: vec![
|
||||
Vertex2D {
|
||||
position: posn_center,
|
||||
color: [1.0, 1.0, 1.0, 1.0],
|
||||
},
|
||||
Vertex2D {
|
||||
position: posn_tl,
|
||||
color: color_r,
|
||||
},
|
||||
Vertex2D {
|
||||
position: posn_t,
|
||||
color: color_o,
|
||||
},
|
||||
Vertex2D {
|
||||
position: posn_tr,
|
||||
color: color_y,
|
||||
},
|
||||
Vertex2D {
|
||||
position: posn_r,
|
||||
color: color_g,
|
||||
},
|
||||
Vertex2D {
|
||||
position: posn_br,
|
||||
color: color_gb,
|
||||
},
|
||||
Vertex2D {
|
||||
position: posn_b,
|
||||
color: color_b,
|
||||
},
|
||||
Vertex2D {
|
||||
position: posn_bl,
|
||||
color: color_i,
|
||||
},
|
||||
Vertex2D {
|
||||
position: posn_l,
|
||||
color: color_v,
|
||||
},
|
||||
],
|
||||
indices: vec![
|
||||
0, 1, 2, // TL
|
||||
0, 2, 3, // T
|
||||
0, 3, 4, // TR
|
||||
0, 4, 5, // R
|
||||
0, 5, 6, // BR
|
||||
0, 6, 7, // B
|
||||
0, 7, 8, // BL
|
||||
0, 8, 1, // L
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
mouse::Interaction::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for Rainbow
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn into(self) -> Element<'a, Message, Renderer<B>> {
|
||||
Element::new(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use iced::{
|
||||
scrollable, Align, Column, Container, Element, Length, Sandbox, Scrollable,
|
||||
Settings, Text,
|
||||
};
|
||||
use rainbow::Rainbow;
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
Example::run(Settings::default())
|
||||
}
|
||||
|
||||
struct Example {
|
||||
scroll: scrollable::State,
|
||||
}
|
||||
|
||||
impl Sandbox for Example {
|
||||
type Message = ();
|
||||
|
||||
fn new() -> Self {
|
||||
Example {
|
||||
scroll: scrollable::State::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Custom 2D geometry - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, _: ()) {}
|
||||
|
||||
fn view(&mut self) -> Element<()> {
|
||||
let content = Column::new()
|
||||
.padding(20)
|
||||
.spacing(20)
|
||||
.max_width(500)
|
||||
.align_items(Align::Start)
|
||||
.push(Rainbow::new())
|
||||
.push(Text::new(
|
||||
"In this example we draw a custom widget Rainbow, using \
|
||||
the Mesh2D primitive. This primitive supplies a list of \
|
||||
triangles, expressed as vertices and indices.",
|
||||
))
|
||||
.push(Text::new(
|
||||
"Move your cursor over it, and see the center vertex \
|
||||
follow you!",
|
||||
))
|
||||
.push(Text::new(
|
||||
"Every Vertex2D defines its own color. You could use the \
|
||||
Mesh2D primitive to render virtually any two-dimensional \
|
||||
geometry for your widget.",
|
||||
));
|
||||
|
||||
let scrollable = Scrollable::new(&mut self.scroll)
|
||||
.push(Container::new(content).width(Length::Fill).center_x());
|
||||
|
||||
Container::new(scrollable)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_y()
|
||||
.into()
|
||||
}
|
||||
}
|
11
examples/integration/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "integration"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced_winit = { path = "../../winit" }
|
||||
iced_wgpu = { path = "../../wgpu" }
|
||||
env_logger = "0.8"
|
18
examples/integration/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
## Integration
|
||||
|
||||
A demonstration of how to integrate Iced in an existing graphical application.
|
||||
|
||||
The __[`main`]__ file contains all the code of the example.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/nicemediocrekodiakbear">
|
||||
<img src="https://thumbs.gfycat.com/NiceMediocreKodiakbear-small.gif">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package integration
|
||||
```
|
||||
|
||||
[`main`]: src/main.rs
|
110
examples/integration/src/controls.rs
Normal file
@ -0,0 +1,110 @@
|
||||
use iced_wgpu::Renderer;
|
||||
use iced_winit::{
|
||||
slider, Align, Clipboard, Color, Column, Command, Element, Length, Program,
|
||||
Row, Slider, Text,
|
||||
};
|
||||
|
||||
pub struct Controls {
|
||||
background_color: Color,
|
||||
sliders: [slider::State; 3],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
BackgroundColorChanged(Color),
|
||||
}
|
||||
|
||||
impl Controls {
|
||||
pub fn new() -> Controls {
|
||||
Controls {
|
||||
background_color: Color::BLACK,
|
||||
sliders: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn background_color(&self) -> Color {
|
||||
self.background_color
|
||||
}
|
||||
}
|
||||
|
||||
impl Program for Controls {
|
||||
type Renderer = Renderer;
|
||||
type Message = Message;
|
||||
type Clipboard = Clipboard;
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Message,
|
||||
_clipboard: &mut Clipboard,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::BackgroundColorChanged(color) => {
|
||||
self.background_color = color;
|
||||
}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message, Renderer> {
|
||||
let [r, g, b] = &mut self.sliders;
|
||||
let background_color = self.background_color;
|
||||
|
||||
let sliders = Row::new()
|
||||
.width(Length::Units(500))
|
||||
.spacing(20)
|
||||
.push(
|
||||
Slider::new(r, 0.0..=1.0, background_color.r, move |r| {
|
||||
Message::BackgroundColorChanged(Color {
|
||||
r,
|
||||
..background_color
|
||||
})
|
||||
})
|
||||
.step(0.01),
|
||||
)
|
||||
.push(
|
||||
Slider::new(g, 0.0..=1.0, background_color.g, move |g| {
|
||||
Message::BackgroundColorChanged(Color {
|
||||
g,
|
||||
..background_color
|
||||
})
|
||||
})
|
||||
.step(0.01),
|
||||
)
|
||||
.push(
|
||||
Slider::new(b, 0.0..=1.0, background_color.b, move |b| {
|
||||
Message::BackgroundColorChanged(Color {
|
||||
b,
|
||||
..background_color
|
||||
})
|
||||
})
|
||||
.step(0.01),
|
||||
);
|
||||
|
||||
Row::new()
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.align_items(Align::End)
|
||||
.push(
|
||||
Column::new()
|
||||
.width(Length::Fill)
|
||||
.align_items(Align::End)
|
||||
.push(
|
||||
Column::new()
|
||||
.padding(10)
|
||||
.spacing(10)
|
||||
.push(
|
||||
Text::new("Background color")
|
||||
.color(Color::WHITE),
|
||||
)
|
||||
.push(sliders)
|
||||
.push(
|
||||
Text::new(format!("{:?}", background_color))
|
||||
.size(14)
|
||||
.color(Color::WHITE),
|
||||
),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
225
examples/integration/src/main.rs
Normal file
@ -0,0 +1,225 @@
|
||||
mod controls;
|
||||
mod scene;
|
||||
|
||||
use controls::Controls;
|
||||
use scene::Scene;
|
||||
|
||||
use iced_wgpu::{wgpu, Backend, Renderer, Settings, Viewport};
|
||||
use iced_winit::{conversion, futures, program, winit, Clipboard, Debug, Size};
|
||||
|
||||
use futures::task::SpawnExt;
|
||||
use winit::{
|
||||
dpi::PhysicalPosition,
|
||||
event::{Event, ModifiersState, WindowEvent},
|
||||
event_loop::{ControlFlow, EventLoop},
|
||||
};
|
||||
|
||||
pub fn main() {
|
||||
env_logger::init();
|
||||
|
||||
// Initialize winit
|
||||
let event_loop = EventLoop::new();
|
||||
let window = winit::window::Window::new(&event_loop).unwrap();
|
||||
|
||||
let physical_size = window.inner_size();
|
||||
let mut viewport = Viewport::with_physical_size(
|
||||
Size::new(physical_size.width, physical_size.height),
|
||||
window.scale_factor(),
|
||||
);
|
||||
let mut cursor_position = PhysicalPosition::new(-1.0, -1.0);
|
||||
let mut modifiers = ModifiersState::default();
|
||||
let mut clipboard = Clipboard::connect(&window);
|
||||
|
||||
// Initialize wgpu
|
||||
let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY);
|
||||
let surface = unsafe { instance.create_surface(&window) };
|
||||
|
||||
let (mut device, queue) = futures::executor::block_on(async {
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||
compatible_surface: Some(&surface),
|
||||
})
|
||||
.await
|
||||
.expect("Request adapter");
|
||||
|
||||
adapter
|
||||
.request_device(
|
||||
&wgpu::DeviceDescriptor {
|
||||
label: None,
|
||||
features: wgpu::Features::empty(),
|
||||
limits: wgpu::Limits::default(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Request device")
|
||||
});
|
||||
|
||||
let format = wgpu::TextureFormat::Bgra8UnormSrgb;
|
||||
|
||||
let mut swap_chain = {
|
||||
let size = window.inner_size();
|
||||
|
||||
device.create_swap_chain(
|
||||
&surface,
|
||||
&wgpu::SwapChainDescriptor {
|
||||
usage: wgpu::TextureUsage::RENDER_ATTACHMENT,
|
||||
format: format,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: wgpu::PresentMode::Mailbox,
|
||||
},
|
||||
)
|
||||
};
|
||||
let mut resized = false;
|
||||
|
||||
// Initialize staging belt and local pool
|
||||
let mut staging_belt = wgpu::util::StagingBelt::new(5 * 1024);
|
||||
let mut local_pool = futures::executor::LocalPool::new();
|
||||
|
||||
// Initialize scene and GUI controls
|
||||
let scene = Scene::new(&mut device);
|
||||
let controls = Controls::new();
|
||||
|
||||
// Initialize iced
|
||||
let mut debug = Debug::new();
|
||||
let mut renderer =
|
||||
Renderer::new(Backend::new(&mut device, Settings::default()));
|
||||
|
||||
let mut state = program::State::new(
|
||||
controls,
|
||||
viewport.logical_size(),
|
||||
conversion::cursor_position(cursor_position, viewport.scale_factor()),
|
||||
&mut renderer,
|
||||
&mut debug,
|
||||
);
|
||||
|
||||
// Run event loop
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
// You should change this if you want to render continuosly
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
match event {
|
||||
Event::WindowEvent { event, .. } => {
|
||||
match event {
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
cursor_position = position;
|
||||
}
|
||||
WindowEvent::ModifiersChanged(new_modifiers) => {
|
||||
modifiers = new_modifiers;
|
||||
}
|
||||
WindowEvent::Resized(new_size) => {
|
||||
viewport = Viewport::with_physical_size(
|
||||
Size::new(new_size.width, new_size.height),
|
||||
window.scale_factor(),
|
||||
);
|
||||
|
||||
resized = true;
|
||||
}
|
||||
WindowEvent::CloseRequested => {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Map window event to iced event
|
||||
if let Some(event) = iced_winit::conversion::window_event(
|
||||
&event,
|
||||
window.scale_factor(),
|
||||
modifiers,
|
||||
) {
|
||||
state.queue_event(event);
|
||||
}
|
||||
}
|
||||
Event::MainEventsCleared => {
|
||||
// If there are events pending
|
||||
if !state.is_queue_empty() {
|
||||
// We update iced
|
||||
let _ = state.update(
|
||||
viewport.logical_size(),
|
||||
conversion::cursor_position(
|
||||
cursor_position,
|
||||
viewport.scale_factor(),
|
||||
),
|
||||
&mut renderer,
|
||||
&mut clipboard,
|
||||
&mut debug,
|
||||
);
|
||||
|
||||
// and request a redraw
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
Event::RedrawRequested(_) => {
|
||||
if resized {
|
||||
let size = window.inner_size();
|
||||
|
||||
swap_chain = device.create_swap_chain(
|
||||
&surface,
|
||||
&wgpu::SwapChainDescriptor {
|
||||
usage: wgpu::TextureUsage::RENDER_ATTACHMENT,
|
||||
format: format,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: wgpu::PresentMode::Mailbox,
|
||||
},
|
||||
);
|
||||
|
||||
resized = false;
|
||||
}
|
||||
|
||||
let frame = swap_chain.get_current_frame().expect("Next frame");
|
||||
|
||||
let mut encoder = device.create_command_encoder(
|
||||
&wgpu::CommandEncoderDescriptor { label: None },
|
||||
);
|
||||
|
||||
let program = state.program();
|
||||
|
||||
{
|
||||
// We clear the frame
|
||||
let mut render_pass = scene.clear(
|
||||
&frame.output.view,
|
||||
&mut encoder,
|
||||
program.background_color(),
|
||||
);
|
||||
|
||||
// Draw the scene
|
||||
scene.draw(&mut render_pass);
|
||||
}
|
||||
|
||||
// And then iced on top
|
||||
let mouse_interaction = renderer.backend_mut().draw(
|
||||
&mut device,
|
||||
&mut staging_belt,
|
||||
&mut encoder,
|
||||
&frame.output.view,
|
||||
&viewport,
|
||||
state.primitive(),
|
||||
&debug.overlay(),
|
||||
);
|
||||
|
||||
// Then we submit the work
|
||||
staging_belt.finish();
|
||||
queue.submit(Some(encoder.finish()));
|
||||
|
||||
// Update the mouse cursor
|
||||
window.set_cursor_icon(
|
||||
iced_winit::conversion::mouse_interaction(
|
||||
mouse_interaction,
|
||||
),
|
||||
);
|
||||
|
||||
// And recall staging buffers
|
||||
local_pool
|
||||
.spawner()
|
||||
.spawn(staging_belt.recall())
|
||||
.expect("Recall staging buffers");
|
||||
|
||||
local_pool.run_until_stalled();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
}
|
99
examples/integration/src/scene.rs
Normal file
@ -0,0 +1,99 @@
|
||||
use iced_wgpu::wgpu;
|
||||
use iced_winit::Color;
|
||||
|
||||
pub struct Scene {
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
pub fn new(device: &wgpu::Device) -> Scene {
|
||||
let pipeline = build_pipeline(device);
|
||||
|
||||
Scene { pipeline }
|
||||
}
|
||||
|
||||
pub fn clear<'a>(
|
||||
&self,
|
||||
target: &'a wgpu::TextureView,
|
||||
encoder: &'a mut wgpu::CommandEncoder,
|
||||
background_color: Color,
|
||||
) -> wgpu::RenderPass<'a> {
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: None,
|
||||
color_attachments: &[wgpu::RenderPassColorAttachment {
|
||||
view: target,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear({
|
||||
let [r, g, b, a] = background_color.into_linear();
|
||||
|
||||
wgpu::Color {
|
||||
r: r as f64,
|
||||
g: g as f64,
|
||||
b: b as f64,
|
||||
a: a as f64,
|
||||
}
|
||||
}),
|
||||
store: true,
|
||||
},
|
||||
}],
|
||||
depth_stencil_attachment: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn draw<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
|
||||
render_pass.set_pipeline(&self.pipeline);
|
||||
render_pass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline {
|
||||
let vs_module =
|
||||
device.create_shader_module(&wgpu::include_spirv!("shader/vert.spv"));
|
||||
|
||||
let fs_module =
|
||||
device.create_shader_module(&wgpu::include_spirv!("shader/frag.spv"));
|
||||
|
||||
let pipeline_layout =
|
||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: None,
|
||||
push_constant_ranges: &[],
|
||||
bind_group_layouts: &[],
|
||||
});
|
||||
|
||||
let pipeline =
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &vs_module,
|
||||
entry_point: "main",
|
||||
buffers: &[],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &fs_module,
|
||||
entry_point: "main",
|
||||
targets: &[wgpu::ColorTargetState {
|
||||
format: wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||
blend: Some(wgpu::BlendState {
|
||||
color: wgpu::BlendComponent::REPLACE,
|
||||
alpha: wgpu::BlendComponent::REPLACE,
|
||||
}),
|
||||
write_mask: wgpu::ColorWrite::ALL,
|
||||
}],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
pipeline
|
||||
}
|
BIN
examples/integration/src/shader/frag.spv
Normal file
BIN
examples/integration/src/shader/vert.spv
Normal file
10
examples/pane_grid/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "pane_grid"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["debug"] }
|
||||
iced_native = { path = "../../native" }
|
28
examples/pane_grid/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
## Pane grid
|
||||
|
||||
A grid of panes that can be split, resized, and reorganized.
|
||||
|
||||
This example showcases the `PaneGrid` widget, which features:
|
||||
|
||||
* Vertical and horizontal splits
|
||||
* Tracking of the last active pane
|
||||
* Mouse-based resizing
|
||||
* Drag and drop to reorganize panes
|
||||
* Hotkey support
|
||||
* Configurable modifier keys
|
||||
* API to perform actions programmatically (`split`, `swap`, `resize`, etc.)
|
||||
|
||||
The __[`main`]__ file contains all the code of the example.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/frailfreshairedaleterrier">
|
||||
<img src="https://thumbs.gfycat.com/FrailFreshAiredaleterrier-small.gif">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package pane_grid
|
||||
```
|
||||
|
||||
[`main`]: src/main.rs
|
479
examples/pane_grid/src/main.rs
Normal file
@ -0,0 +1,479 @@
|
||||
use iced::{
|
||||
button, executor, keyboard, pane_grid, scrollable, Align, Application,
|
||||
Button, Clipboard, Color, Column, Command, Container, Element,
|
||||
HorizontalAlignment, Length, PaneGrid, Row, Scrollable, Settings,
|
||||
Subscription, Text,
|
||||
};
|
||||
use iced_native::{event, subscription, Event};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
Example::run(Settings::default())
|
||||
}
|
||||
|
||||
struct Example {
|
||||
panes: pane_grid::State<Pane>,
|
||||
panes_created: usize,
|
||||
focus: Option<pane_grid::Pane>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Message {
|
||||
Split(pane_grid::Axis, pane_grid::Pane),
|
||||
SplitFocused(pane_grid::Axis),
|
||||
FocusAdjacent(pane_grid::Direction),
|
||||
Clicked(pane_grid::Pane),
|
||||
Dragged(pane_grid::DragEvent),
|
||||
Resized(pane_grid::ResizeEvent),
|
||||
TogglePin(pane_grid::Pane),
|
||||
Close(pane_grid::Pane),
|
||||
CloseFocused,
|
||||
}
|
||||
|
||||
impl Application for Example {
|
||||
type Message = Message;
|
||||
type Executor = executor::Default;
|
||||
type Flags = ();
|
||||
|
||||
fn new(_flags: ()) -> (Self, Command<Message>) {
|
||||
let (panes, _) = pane_grid::State::new(Pane::new(0));
|
||||
|
||||
(
|
||||
Example {
|
||||
panes,
|
||||
panes_created: 1,
|
||||
focus: None,
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Pane grid - Iced")
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Message,
|
||||
_clipboard: &mut Clipboard,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::Split(axis, pane) => {
|
||||
let result = self.panes.split(
|
||||
axis,
|
||||
&pane,
|
||||
Pane::new(self.panes_created),
|
||||
);
|
||||
|
||||
if let Some((pane, _)) = result {
|
||||
self.focus = Some(pane);
|
||||
}
|
||||
|
||||
self.panes_created += 1;
|
||||
}
|
||||
Message::SplitFocused(axis) => {
|
||||
if let Some(pane) = self.focus {
|
||||
let result = self.panes.split(
|
||||
axis,
|
||||
&pane,
|
||||
Pane::new(self.panes_created),
|
||||
);
|
||||
|
||||
if let Some((pane, _)) = result {
|
||||
self.focus = Some(pane);
|
||||
}
|
||||
|
||||
self.panes_created += 1;
|
||||
}
|
||||
}
|
||||
Message::FocusAdjacent(direction) => {
|
||||
if let Some(pane) = self.focus {
|
||||
if let Some(adjacent) =
|
||||
self.panes.adjacent(&pane, direction)
|
||||
{
|
||||
self.focus = Some(adjacent);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Clicked(pane) => {
|
||||
self.focus = Some(pane);
|
||||
}
|
||||
Message::Resized(pane_grid::ResizeEvent { split, ratio }) => {
|
||||
self.panes.resize(&split, ratio);
|
||||
}
|
||||
Message::Dragged(pane_grid::DragEvent::Dropped {
|
||||
pane,
|
||||
target,
|
||||
}) => {
|
||||
self.panes.swap(&pane, &target);
|
||||
}
|
||||
Message::Dragged(_) => {}
|
||||
Message::TogglePin(pane) => {
|
||||
if let Some(Pane { is_pinned, .. }) = self.panes.get_mut(&pane)
|
||||
{
|
||||
*is_pinned = !*is_pinned;
|
||||
}
|
||||
}
|
||||
Message::Close(pane) => {
|
||||
if let Some((_, sibling)) = self.panes.close(&pane) {
|
||||
self.focus = Some(sibling);
|
||||
}
|
||||
}
|
||||
Message::CloseFocused => {
|
||||
if let Some(pane) = self.focus {
|
||||
if let Some(Pane { is_pinned, .. }) = self.panes.get(&pane)
|
||||
{
|
||||
if !is_pinned {
|
||||
if let Some((_, sibling)) = self.panes.close(&pane)
|
||||
{
|
||||
self.focus = Some(sibling);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
subscription::events_with(|event, status| {
|
||||
if let event::Status::Captured = status {
|
||||
return None;
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
modifiers,
|
||||
key_code,
|
||||
}) if modifiers.is_command_pressed() => handle_hotkey(key_code),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let focus = self.focus;
|
||||
let total_panes = self.panes.len();
|
||||
|
||||
let pane_grid = PaneGrid::new(&mut self.panes, |id, pane| {
|
||||
let is_focused = focus == Some(id);
|
||||
|
||||
let text = if pane.is_pinned { "Unpin" } else { "Pin" };
|
||||
let pin_button =
|
||||
Button::new(&mut pane.pin_button, Text::new(text).size(14))
|
||||
.on_press(Message::TogglePin(id))
|
||||
.style(style::Button::Pin)
|
||||
.padding(3);
|
||||
|
||||
let title = Row::with_children(vec![
|
||||
pin_button.into(),
|
||||
Text::new("Pane").into(),
|
||||
Text::new(pane.content.id.to_string())
|
||||
.color(if is_focused {
|
||||
PANE_ID_COLOR_FOCUSED
|
||||
} else {
|
||||
PANE_ID_COLOR_UNFOCUSED
|
||||
})
|
||||
.into(),
|
||||
])
|
||||
.spacing(5);
|
||||
|
||||
let title_bar = pane_grid::TitleBar::new(title)
|
||||
.controls(pane.controls.view(id, total_panes, pane.is_pinned))
|
||||
.padding(10)
|
||||
.style(style::TitleBar { is_focused });
|
||||
|
||||
pane_grid::Content::new(pane.content.view(
|
||||
id,
|
||||
total_panes,
|
||||
pane.is_pinned,
|
||||
))
|
||||
.title_bar(title_bar)
|
||||
.style(style::Pane { is_focused })
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.spacing(10)
|
||||
.on_click(Message::Clicked)
|
||||
.on_drag(Message::Dragged)
|
||||
.on_resize(10, Message::Resized);
|
||||
|
||||
Container::new(pane_grid)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(10)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
const PANE_ID_COLOR_UNFOCUSED: Color = Color::from_rgb(
|
||||
0xFF as f32 / 255.0,
|
||||
0xC7 as f32 / 255.0,
|
||||
0xC7 as f32 / 255.0,
|
||||
);
|
||||
const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb(
|
||||
0xFF as f32 / 255.0,
|
||||
0x47 as f32 / 255.0,
|
||||
0x47 as f32 / 255.0,
|
||||
);
|
||||
|
||||
fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> {
|
||||
use keyboard::KeyCode;
|
||||
use pane_grid::{Axis, Direction};
|
||||
|
||||
let direction = match key_code {
|
||||
KeyCode::Up => Some(Direction::Up),
|
||||
KeyCode::Down => Some(Direction::Down),
|
||||
KeyCode::Left => Some(Direction::Left),
|
||||
KeyCode::Right => Some(Direction::Right),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
match key_code {
|
||||
KeyCode::V => Some(Message::SplitFocused(Axis::Vertical)),
|
||||
KeyCode::H => Some(Message::SplitFocused(Axis::Horizontal)),
|
||||
KeyCode::W => Some(Message::CloseFocused),
|
||||
_ => direction.map(Message::FocusAdjacent),
|
||||
}
|
||||
}
|
||||
|
||||
struct Pane {
|
||||
pub is_pinned: bool,
|
||||
pub pin_button: button::State,
|
||||
pub content: Content,
|
||||
pub controls: Controls,
|
||||
}
|
||||
|
||||
struct Content {
|
||||
id: usize,
|
||||
scroll: scrollable::State,
|
||||
split_horizontally: button::State,
|
||||
split_vertically: button::State,
|
||||
close: button::State,
|
||||
}
|
||||
|
||||
struct Controls {
|
||||
close: button::State,
|
||||
}
|
||||
|
||||
impl Pane {
|
||||
fn new(id: usize) -> Self {
|
||||
Self {
|
||||
is_pinned: false,
|
||||
pin_button: button::State::new(),
|
||||
content: Content::new(id),
|
||||
controls: Controls::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Content {
|
||||
fn new(id: usize) -> Self {
|
||||
Content {
|
||||
id,
|
||||
scroll: scrollable::State::new(),
|
||||
split_horizontally: button::State::new(),
|
||||
split_vertically: button::State::new(),
|
||||
close: button::State::new(),
|
||||
}
|
||||
}
|
||||
fn view(
|
||||
&mut self,
|
||||
pane: pane_grid::Pane,
|
||||
total_panes: usize,
|
||||
is_pinned: bool,
|
||||
) -> Element<Message> {
|
||||
let Content {
|
||||
scroll,
|
||||
split_horizontally,
|
||||
split_vertically,
|
||||
close,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let button = |state, label, message, style| {
|
||||
Button::new(
|
||||
state,
|
||||
Text::new(label)
|
||||
.width(Length::Fill)
|
||||
.horizontal_alignment(HorizontalAlignment::Center)
|
||||
.size(16),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding(8)
|
||||
.on_press(message)
|
||||
.style(style)
|
||||
};
|
||||
|
||||
let mut controls = Column::new()
|
||||
.spacing(5)
|
||||
.max_width(150)
|
||||
.push(button(
|
||||
split_horizontally,
|
||||
"Split horizontally",
|
||||
Message::Split(pane_grid::Axis::Horizontal, pane),
|
||||
style::Button::Primary,
|
||||
))
|
||||
.push(button(
|
||||
split_vertically,
|
||||
"Split vertically",
|
||||
Message::Split(pane_grid::Axis::Vertical, pane),
|
||||
style::Button::Primary,
|
||||
));
|
||||
|
||||
if total_panes > 1 && !is_pinned {
|
||||
controls = controls.push(button(
|
||||
close,
|
||||
"Close",
|
||||
Message::Close(pane),
|
||||
style::Button::Destructive,
|
||||
));
|
||||
}
|
||||
|
||||
let content = Scrollable::new(scroll)
|
||||
.width(Length::Fill)
|
||||
.spacing(10)
|
||||
.align_items(Align::Center)
|
||||
.push(controls);
|
||||
|
||||
Container::new(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(5)
|
||||
.center_y()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Controls {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
close: button::State::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(
|
||||
&mut self,
|
||||
pane: pane_grid::Pane,
|
||||
total_panes: usize,
|
||||
is_pinned: bool,
|
||||
) -> Element<Message> {
|
||||
let mut button =
|
||||
Button::new(&mut self.close, Text::new("Close").size(14))
|
||||
.style(style::Button::Control)
|
||||
.padding(3);
|
||||
if total_panes > 1 && !is_pinned {
|
||||
button = button.on_press(Message::Close(pane));
|
||||
}
|
||||
button.into()
|
||||
}
|
||||
}
|
||||
|
||||
mod style {
|
||||
use crate::PANE_ID_COLOR_FOCUSED;
|
||||
use iced::{button, container, Background, Color, Vector};
|
||||
|
||||
const SURFACE: Color = Color::from_rgb(
|
||||
0xF2 as f32 / 255.0,
|
||||
0xF3 as f32 / 255.0,
|
||||
0xF5 as f32 / 255.0,
|
||||
);
|
||||
|
||||
const ACTIVE: Color = Color::from_rgb(
|
||||
0x72 as f32 / 255.0,
|
||||
0x89 as f32 / 255.0,
|
||||
0xDA as f32 / 255.0,
|
||||
);
|
||||
|
||||
const HOVERED: Color = Color::from_rgb(
|
||||
0x67 as f32 / 255.0,
|
||||
0x7B as f32 / 255.0,
|
||||
0xC4 as f32 / 255.0,
|
||||
);
|
||||
|
||||
pub struct TitleBar {
|
||||
pub is_focused: bool,
|
||||
}
|
||||
|
||||
impl container::StyleSheet for TitleBar {
|
||||
fn style(&self) -> container::Style {
|
||||
let pane = Pane {
|
||||
is_focused: self.is_focused,
|
||||
}
|
||||
.style();
|
||||
|
||||
container::Style {
|
||||
text_color: Some(Color::WHITE),
|
||||
background: Some(pane.border_color.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Pane {
|
||||
pub is_focused: bool,
|
||||
}
|
||||
|
||||
impl container::StyleSheet for Pane {
|
||||
fn style(&self) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(SURFACE)),
|
||||
border_width: 2.0,
|
||||
border_color: if self.is_focused {
|
||||
Color::BLACK
|
||||
} else {
|
||||
Color::from_rgb(0.7, 0.7, 0.7)
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Button {
|
||||
Primary,
|
||||
Destructive,
|
||||
Control,
|
||||
Pin,
|
||||
}
|
||||
|
||||
impl button::StyleSheet for Button {
|
||||
fn active(&self) -> button::Style {
|
||||
let (background, text_color) = match self {
|
||||
Button::Primary => (Some(ACTIVE), Color::WHITE),
|
||||
Button::Destructive => {
|
||||
(None, Color::from_rgb8(0xFF, 0x47, 0x47))
|
||||
}
|
||||
Button::Control => (Some(PANE_ID_COLOR_FOCUSED), Color::WHITE),
|
||||
Button::Pin => (Some(ACTIVE), Color::WHITE),
|
||||
};
|
||||
|
||||
button::Style {
|
||||
text_color,
|
||||
background: background.map(Background::Color),
|
||||
border_radius: 5.0,
|
||||
shadow_offset: Vector::new(0.0, 0.0),
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self) -> button::Style {
|
||||
let active = self.active();
|
||||
|
||||
let background = match self {
|
||||
Button::Primary => Some(HOVERED),
|
||||
Button::Destructive => Some(Color {
|
||||
a: 0.2,
|
||||
..active.text_color
|
||||
}),
|
||||
Button::Control => Some(PANE_ID_COLOR_FOCUSED),
|
||||
Button::Pin => Some(HOVERED),
|
||||
};
|
||||
|
||||
button::Style {
|
||||
background: background.map(Background::Color),
|
||||
..active
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
examples/pick_list/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "pick_list"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["debug"] }
|
18
examples/pick_list/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
## Pick-list
|
||||
|
||||
A dropdown list of selectable options.
|
||||
|
||||
It displays and positions an overlay based on the window position of the widget.
|
||||
|
||||
The __[`main`]__ file contains all the code of the example.
|
||||
|
||||
<div align="center">
|
||||
<img src="https://user-images.githubusercontent.com/518289/87125075-2c232e80-c28a-11ea-95c2-769c610b8843.gif">
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package pick_list
|
||||
```
|
||||
|
||||
[`main`]: src/main.rs
|
113
examples/pick_list/src/main.rs
Normal file
@ -0,0 +1,113 @@
|
||||
use iced::{
|
||||
pick_list, scrollable, Align, Container, Element, Length, PickList,
|
||||
Sandbox, Scrollable, Settings, Space, Text,
|
||||
};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
Example::run(Settings::default())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Example {
|
||||
scroll: scrollable::State,
|
||||
pick_list: pick_list::State<Language>,
|
||||
selected_language: Language,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Message {
|
||||
LanguageSelected(Language),
|
||||
}
|
||||
|
||||
impl Sandbox for Example {
|
||||
type Message = Message;
|
||||
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Pick list - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::LanguageSelected(language) => {
|
||||
self.selected_language = language;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let pick_list = PickList::new(
|
||||
&mut self.pick_list,
|
||||
&Language::ALL[..],
|
||||
Some(self.selected_language),
|
||||
Message::LanguageSelected,
|
||||
);
|
||||
|
||||
let mut content = Scrollable::new(&mut self.scroll)
|
||||
.width(Length::Fill)
|
||||
.align_items(Align::Center)
|
||||
.spacing(10)
|
||||
.push(Space::with_height(Length::Units(600)))
|
||||
.push(Text::new("Which is your favorite language?"))
|
||||
.push(pick_list);
|
||||
|
||||
content = content.push(Space::with_height(Length::Units(600)));
|
||||
|
||||
Container::new(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_x()
|
||||
.center_y()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Language {
|
||||
Rust,
|
||||
Elm,
|
||||
Ruby,
|
||||
Haskell,
|
||||
C,
|
||||
Javascript,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Language {
|
||||
const ALL: [Language; 7] = [
|
||||
Language::C,
|
||||
Language::Elm,
|
||||
Language::Ruby,
|
||||
Language::Haskell,
|
||||
Language::Rust,
|
||||
Language::Javascript,
|
||||
Language::Other,
|
||||
];
|
||||
}
|
||||
|
||||
impl Default for Language {
|
||||
fn default() -> Language {
|
||||
Language::Rust
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Language {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Language::Rust => "Rust",
|
||||
Language::Elm => "Elm",
|
||||
Language::Ruby => "Ruby",
|
||||
Language::Haskell => "Haskell",
|
||||
Language::C => "C",
|
||||
Language::Javascript => "Javascript",
|
||||
Language::Other => "Some other language",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
22
examples/pokedex/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "pokedex"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["image", "debug", "tokio_old"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.10.2"
|
||||
features = ["json"]
|
||||
|
||||
[dependencies.rand]
|
||||
version = "0.7"
|
||||
features = ["wasm-bindgen"]
|
17
examples/pokedex/README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Pokédex
|
||||
An application that loads a random Pokédex entry using the [PokéAPI].
|
||||
|
||||
All the example code can be found in the __[`main`](src/main.rs)__ file.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/aggressivedarkelephantseal-rust-gui">
|
||||
<img src="https://thumbs.gfycat.com/AggressiveDarkElephantseal-small.gif" height="400px">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it on native platforms with `cargo run`:
|
||||
```
|
||||
cargo run --package pokedex
|
||||
```
|
||||
|
||||
[PokéAPI]: https://pokeapi.co/
|
273
examples/pokedex/src/main.rs
Normal file
@ -0,0 +1,273 @@
|
||||
use iced::{
|
||||
button, futures, image, Align, Application, Button, Clipboard, Column,
|
||||
Command, Container, Element, Length, Row, Settings, Text,
|
||||
};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
Pokedex::run(Settings::default())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Pokedex {
|
||||
Loading,
|
||||
Loaded {
|
||||
pokemon: Pokemon,
|
||||
search: button::State,
|
||||
},
|
||||
Errored {
|
||||
error: Error,
|
||||
try_again: button::State,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
PokemonFound(Result<Pokemon, Error>),
|
||||
Search,
|
||||
}
|
||||
|
||||
impl Application for Pokedex {
|
||||
type Executor = iced::executor::Default;
|
||||
type Message = Message;
|
||||
type Flags = ();
|
||||
|
||||
fn new(_flags: ()) -> (Pokedex, Command<Message>) {
|
||||
(
|
||||
Pokedex::Loading,
|
||||
Command::perform(Pokemon::search(), Message::PokemonFound),
|
||||
)
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
let subtitle = match self {
|
||||
Pokedex::Loading => "Loading",
|
||||
Pokedex::Loaded { pokemon, .. } => &pokemon.name,
|
||||
Pokedex::Errored { .. } => "Whoops!",
|
||||
};
|
||||
|
||||
format!("{} - Pokédex", subtitle)
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Message,
|
||||
_clipboard: &mut Clipboard,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::PokemonFound(Ok(pokemon)) => {
|
||||
*self = Pokedex::Loaded {
|
||||
pokemon,
|
||||
search: button::State::new(),
|
||||
};
|
||||
|
||||
Command::none()
|
||||
}
|
||||
Message::PokemonFound(Err(error)) => {
|
||||
*self = Pokedex::Errored {
|
||||
error,
|
||||
try_again: button::State::new(),
|
||||
};
|
||||
|
||||
Command::none()
|
||||
}
|
||||
Message::Search => match self {
|
||||
Pokedex::Loading => Command::none(),
|
||||
_ => {
|
||||
*self = Pokedex::Loading;
|
||||
|
||||
Command::perform(Pokemon::search(), Message::PokemonFound)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let content = match self {
|
||||
Pokedex::Loading => Column::new()
|
||||
.width(Length::Shrink)
|
||||
.push(Text::new("Searching for Pokémon...").size(40)),
|
||||
Pokedex::Loaded { pokemon, search } => Column::new()
|
||||
.max_width(500)
|
||||
.spacing(20)
|
||||
.align_items(Align::End)
|
||||
.push(pokemon.view())
|
||||
.push(
|
||||
button(search, "Keep searching!").on_press(Message::Search),
|
||||
),
|
||||
Pokedex::Errored { try_again, .. } => Column::new()
|
||||
.spacing(20)
|
||||
.align_items(Align::End)
|
||||
.push(Text::new("Whoops! Something went wrong...").size(40))
|
||||
.push(button(try_again, "Try again").on_press(Message::Search)),
|
||||
};
|
||||
|
||||
Container::new(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_x()
|
||||
.center_y()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Pokemon {
|
||||
number: u16,
|
||||
name: String,
|
||||
description: String,
|
||||
image: image::Handle,
|
||||
image_viewer: image::viewer::State,
|
||||
}
|
||||
|
||||
impl Pokemon {
|
||||
const TOTAL: u16 = 807;
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
Row::new()
|
||||
.spacing(20)
|
||||
.align_items(Align::Center)
|
||||
.push(image::Viewer::new(
|
||||
&mut self.image_viewer,
|
||||
self.image.clone(),
|
||||
))
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.push(
|
||||
Row::new()
|
||||
.align_items(Align::Center)
|
||||
.spacing(20)
|
||||
.push(
|
||||
Text::new(&self.name)
|
||||
.size(30)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(
|
||||
Text::new(format!("#{}", self.number))
|
||||
.size(20)
|
||||
.color([0.5, 0.5, 0.5]),
|
||||
),
|
||||
)
|
||||
.push(Text::new(&self.description)),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
async fn search() -> Result<Pokemon, Error> {
|
||||
use rand::Rng;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Entry {
|
||||
id: u32,
|
||||
name: String,
|
||||
flavor_text_entries: Vec<FlavorText>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FlavorText {
|
||||
flavor_text: String,
|
||||
language: Language,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Language {
|
||||
name: String,
|
||||
}
|
||||
|
||||
let id = {
|
||||
let mut rng = rand::rngs::OsRng::default();
|
||||
|
||||
rng.gen_range(0, Pokemon::TOTAL)
|
||||
};
|
||||
|
||||
let fetch_entry = async {
|
||||
let url =
|
||||
format!("https://pokeapi.co/api/v2/pokemon-species/{}", id);
|
||||
|
||||
reqwest::get(&url).await?.json().await
|
||||
};
|
||||
|
||||
let (entry, image): (Entry, _) =
|
||||
futures::future::try_join(fetch_entry, Self::fetch_image(id))
|
||||
.await?;
|
||||
|
||||
let description = entry
|
||||
.flavor_text_entries
|
||||
.iter()
|
||||
.filter(|text| text.language.name == "en")
|
||||
.next()
|
||||
.ok_or(Error::LanguageError)?;
|
||||
|
||||
Ok(Pokemon {
|
||||
number: id,
|
||||
name: entry.name.to_uppercase(),
|
||||
description: description
|
||||
.flavor_text
|
||||
.chars()
|
||||
.map(|c| if c.is_control() { ' ' } else { c })
|
||||
.collect(),
|
||||
image,
|
||||
image_viewer: image::viewer::State::new(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_image(id: u16) -> Result<image::Handle, reqwest::Error> {
|
||||
let url = format!(
|
||||
"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{}.png",
|
||||
id
|
||||
);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let bytes = reqwest::get(&url).await?.bytes().await?;
|
||||
|
||||
Ok(image::Handle::from_memory(bytes.as_ref().to_vec()))
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Ok(image::Handle::from_path(url))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Error {
|
||||
APIError,
|
||||
LanguageError,
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(error: reqwest::Error) -> Error {
|
||||
dbg!(error);
|
||||
|
||||
Error::APIError
|
||||
}
|
||||
}
|
||||
|
||||
fn button<'a>(state: &'a mut button::State, text: &str) -> Button<'a, Message> {
|
||||
Button::new(state, Text::new(text))
|
||||
.padding(10)
|
||||
.style(style::Button::Primary)
|
||||
}
|
||||
|
||||
mod style {
|
||||
use iced::{button, Background, Color, Vector};
|
||||
|
||||
pub enum Button {
|
||||
Primary,
|
||||
}
|
||||
|
||||
impl button::StyleSheet for Button {
|
||||
fn active(&self) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(match self {
|
||||
Button::Primary => Color::from_rgb(0.11, 0.42, 0.87),
|
||||
})),
|
||||
border_radius: 12.0,
|
||||
shadow_offset: Vector::new(1.0, 1.0),
|
||||
text_color: Color::WHITE,
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
examples/progress_bar/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "progress_bar"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../.." }
|
18
examples/progress_bar/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
## Progress bar
|
||||
|
||||
A simple progress bar that can be filled by using a slider.
|
||||
|
||||
The __[`main`]__ file contains all the code of the example.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/importantdevotedhammerheadbird">
|
||||
<img src="https://thumbs.gfycat.com/ImportantDevotedHammerheadbird-small.gif">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package progress_bar
|
||||
```
|
||||
|
||||
[`main`]: src/main.rs
|
50
examples/progress_bar/src/main.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use iced::{slider, Column, Element, ProgressBar, Sandbox, Settings, Slider};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
Progress::run(Settings::default())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Progress {
|
||||
value: f32,
|
||||
progress_bar_slider: slider::State,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Message {
|
||||
SliderChanged(f32),
|
||||
}
|
||||
|
||||
impl Sandbox for Progress {
|
||||
type Message = Message;
|
||||
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("A simple Progressbar")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::SliderChanged(x) => self.value = x,
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
Column::new()
|
||||
.padding(20)
|
||||
.push(ProgressBar::new(0.0..=100.0, self.value))
|
||||
.push(
|
||||
Slider::new(
|
||||
&mut self.progress_bar_slider,
|
||||
0.0..=100.0,
|
||||
self.value,
|
||||
Message::SliderChanged,
|
||||
)
|
||||
.step(0.01),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
9
examples/qr_code/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "qr_code"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["qr_code"] }
|