Compare commits
839 Commits
style-0.1.
...
master
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 | ||
|
36bdc0be1a | ||
|
d3572e1b81 | ||
|
e19a07d400 |
5
.github/workflows/build.yml
vendored
@ -11,6 +11,11 @@ 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
|
||||
|
10
.github/workflows/test.yml
vendored
@ -12,6 +12,12 @@ 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
|
||||
@ -27,3 +33,7 @@ jobs:
|
||||
- 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
|
||||
|
2
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
/target
|
||||
target/
|
||||
pkg/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
|
155
CHANGELOG.md
@ -6,6 +6,154 @@ 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
|
||||
@ -59,7 +207,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
[`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/0.1/wgpu
|
||||
[`iced_wgpu`]: https://github.com/hecrj/iced/tree/master/wgpu
|
||||
|
||||
|
||||
## [0.1.0-beta] - 2019-11-25
|
||||
@ -71,7 +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...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
|
||||
|
50
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "iced"
|
||||
version = "0.1.0"
|
||||
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,18 +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" }
|
||||
@ -32,6 +53,9 @@ maintenance = { status = "actively-developed" }
|
||||
members = [
|
||||
"core",
|
||||
"futures",
|
||||
"graphics",
|
||||
"glow",
|
||||
"glutin",
|
||||
"native",
|
||||
"style",
|
||||
"web",
|
||||
@ -39,29 +63,45 @@ members = [
|
||||
"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_futures = { version = "0.1", path = "futures" }
|
||||
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", path = "winit" }
|
||||
iced_wgpu = { version = "0.2", 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.2", path = "web" }
|
||||
iced_web = { version = "0.4", path = "web" }
|
||||
|
||||
[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`]
|
||||
[`iced_core`] holds basic reusable types of the public API. For instance, basic data types like `Point`, `Rectangle`, `Length`, etc.
|
||||
## 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:
|
||||
|
||||
This crate is meant to be a starting point for an Iced runtime.
|
||||
- [`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.
|
||||
|
||||
### [`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
|
||||
<p align="center">
|
||||
<img alt="The foundations" src="docs/graphs/foundations.png" width="50%">
|
||||
</p>
|
||||
|
||||
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 `Backend` 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.
|
||||
## The native target
|
||||
The native side of the ecosystem is split into two different groups: __renderers__ and __shells__.
|
||||
|
||||
[`druid`]: https://github.com/xi-editor/druid
|
||||
[`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle
|
||||
<p align="center">
|
||||
<img alt="The native target" src="docs/graphs/native.png" width="80%">
|
||||
</p>
|
||||
|
||||
### [`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.
|
||||
### 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.
|
||||
|
||||
The crate is currently a simple abstraction layer over [`dodrio`].
|
||||
Currently, there are two different official renderers:
|
||||
|
||||
[`dodrio`]: https://github.com/fitzgen/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+.
|
||||
|
||||
### [`iced_wgpu`]
|
||||
[`iced_wgpu`] is a [`wgpu`] renderer for [`iced_native`]. For now, it is the default renderer of Iced in native platforms.
|
||||
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.
|
||||
|
||||
[`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].
|
||||
### Shells
|
||||
The widgets of a graphical user _interface_ are interactive. __Shells__ gather and process user interactions in an event loop.
|
||||
|
||||
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.
|
||||
- Clip areas, useful to implement scrollables or hide overflowing content.
|
||||
- Images and SVG, loaded from memory or the file system.
|
||||
- Meshes of triangles, useful to draw geometry freely.
|
||||
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].
|
||||
|
||||
[`wgpu`]: https://github.com/gfx-rs/wgpu-rs
|
||||
[WebGPU API]: https://gpuweb.github.io/gpuweb/
|
||||
[`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph
|
||||
As of now, there are two official shells:
|
||||
|
||||
### [`iced_winit`]
|
||||
[`iced_winit`] offers some convenient abstractions on top of [`iced_native`] to quickstart development when using [`winit`].
|
||||
- [`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].
|
||||
|
||||
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.
|
||||
## The web target
|
||||
The Web platform provides all the abstractions necessary to draw widgets and gather users interactions.
|
||||
|
||||
[`winit`]: https://github.com/rust-windowing/winit
|
||||
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.
|
||||
|
||||
### [`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/
|
||||
|
32
README.md
@ -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"
|
||||
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
|
||||
|
23
ROADMAP.md
@ -6,17 +6,19 @@ 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
|
||||
@ -25,6 +27,8 @@ Once a step is completed, it is collapsed and added to this list:
|
||||
[#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.
|
||||
@ -35,17 +39,6 @@ This approach should also allow us to perform custom optimizations for this part
|
||||
|
||||
[#27]: https://github.com/hecrj/iced/issues/27
|
||||
|
||||
### 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.
|
||||
|
||||
@ -55,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.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "iced_core"
|
||||
version = "0.2.0"
|
||||
version = "0.4.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "The essential concepts of Iced"
|
||||
@ -8,3 +8,7 @@ license = "MIT"
|
||||
repository = "https://github.com/hecrj/iced"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[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.2"
|
||||
iced_core = "0.4"
|
||||
```
|
||||
|
||||
__Iced moves fast and the `master` branch can contain breaking changes!__ If
|
||||
|
@ -13,3 +13,9 @@ impl From<Color> for Background {
|
||||
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,
|
||||
}
|
||||
|
||||
@ -33,23 +39,47 @@ impl Color {
|
||||
a: 0.0,
|
||||
};
|
||||
|
||||
/// Creates a [`Color`] from its RGB components.
|
||||
/// 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 { r, g, b, a: 1.0 }
|
||||
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.
|
||||
///
|
||||
/// [`Color`]: struct.Color.html
|
||||
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.
|
||||
///
|
||||
/// [`Color`]: struct.Color.html
|
||||
pub fn from_rgba8(r: u8, g: u8, b: u8, a: f32) -> Color {
|
||||
Color {
|
||||
r: f32::from(r) / 255.0,
|
||||
@ -60,8 +90,6 @@ impl Color {
|
||||
}
|
||||
|
||||
/// Converts the [`Color`] into its linear values.
|
||||
///
|
||||
/// [`Color`]: struct.Color.html
|
||||
pub fn into_linear(self) -> [f32; 4] {
|
||||
// As described in:
|
||||
// https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
|
||||
@ -80,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
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -16,3 +16,9 @@ pub enum Font {
|
||||
bytes: &'static [u8],
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for Font {
|
||||
fn default() -> Font {
|
||||
Font::Default
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
//! Reuse basic keyboard types.
|
||||
mod event;
|
||||
mod key_code;
|
||||
mod modifiers_state;
|
||||
mod modifiers;
|
||||
|
||||
pub use event::Event;
|
||||
pub use key_code::KeyCode;
|
||||
pub use modifiers_state::ModifiersState;
|
||||
pub use modifiers::Modifiers;
|
||||
|
@ -1,5 +1,4 @@
|
||||
use super::{KeyCode, ModifiersState};
|
||||
use crate::input::ButtonState;
|
||||
use super::{KeyCode, Modifiers};
|
||||
|
||||
/// A keyboard event.
|
||||
///
|
||||
@ -9,18 +8,27 @@ use crate::input::ButtonState;
|
||||
/// [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: ModifiersState,
|
||||
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
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
/// The current state of the keyboard modifiers.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct ModifiersState {
|
||||
/// 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 ModifiersState {
|
||||
/// Returns true if the current [`ModifiersState`] has at least the same
|
||||
/// modifiers enabled as the given value, and false otherwise.
|
||||
///
|
||||
/// [`ModifiersState`]: struct.ModifiersState.html
|
||||
pub fn matches(&self, modifiers: ModifiersState) -> 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
|
||||
}
|
||||
}
|
@ -26,8 +26,6 @@ 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,
|
||||
|
@ -1,11 +1,11 @@
|
||||
//! 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
|
||||
@ -15,12 +15,14 @@
|
||||
#![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;
|
||||
@ -31,6 +33,7 @@ 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;
|
||||
|
@ -1,9 +1,8 @@
|
||||
//! Build mouse events.
|
||||
//! Reuse basic mouse types.
|
||||
mod button;
|
||||
mod event;
|
||||
|
||||
pub mod click;
|
||||
mod interaction;
|
||||
|
||||
pub use button::Button;
|
||||
pub use click::Click;
|
||||
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,
|
||||
@ -12,20 +12,14 @@ pub struct Point {
|
||||
|
||||
impl Point {
|
||||
/// The origin (i.e. a [`Point`] at (0, 0)).
|
||||
///
|
||||
/// [`Point`]: struct.Point.html
|
||||
pub const ORIGIN: Point = Point::new(0.0, 0.0);
|
||||
|
||||
/// Creates a new [`Point`] with the given coordinates.
|
||||
///
|
||||
/// [`Point`]: struct.Point.html
|
||||
pub const fn new(x: f32, y: f32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
/// Computes the distance to another [`Point`].
|
||||
///
|
||||
/// [`Point`]: struct.Point.html
|
||||
pub fn distance(&self, to: Point) -> f32 {
|
||||
let a = self.x - to.x;
|
||||
let b = self.y - to.y;
|
||||
@ -46,6 +40,12 @@ impl From<[u16; 2]> for Point {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Point> for [f32; 2] {
|
||||
fn from(point: Point) -> [f32; 2] {
|
||||
[point.x, point.y]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add<Vector> for Point {
|
||||
type Output = Self;
|
||||
|
||||
@ -67,3 +67,11 @@ impl std::ops::Sub<Vector> for Point {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,10 +17,56 @@ 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
|
||||
@ -29,8 +75,6 @@ impl Rectangle<f32> {
|
||||
}
|
||||
|
||||
/// Computes the intersection with the given [`Rectangle`].
|
||||
///
|
||||
/// [`Rectangle`]: struct.Rectangle.html
|
||||
pub fn intersection(
|
||||
&self,
|
||||
other: &Rectangle<f32>,
|
||||
@ -55,17 +99,27 @@ impl Rectangle<f32> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -81,13 +135,17 @@ impl From<Rectangle<u32>> for Rectangle<f32> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rectangle<f32>> for Rectangle<u32> {
|
||||
fn from(rectangle: Rectangle<f32>) -> Rectangle<u32> {
|
||||
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: rectangle.x as u32,
|
||||
y: rectangle.y as u32,
|
||||
width: rectangle.width.ceil() as u32,
|
||||
height: rectangle.height.ceil() as u32,
|
||||
x: self.x + translation.x,
|
||||
y: self.y + translation.y,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +1,37 @@
|
||||
use crate::{Padding, Vector};
|
||||
use std::f32;
|
||||
|
||||
/// An amount of space in 2 dimensions.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Size {
|
||||
pub struct Size<T = f32> {
|
||||
/// The width.
|
||||
pub width: f32,
|
||||
pub width: T,
|
||||
/// The height.
|
||||
pub height: f32,
|
||||
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.
|
||||
///
|
||||
/// [`Size`]: struct.Size.html
|
||||
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.
|
||||
///
|
||||
/// [`Size`]: struct.Size.html
|
||||
pub const INFINITY: Size = Size::new(f32::INFINITY, f32::INFINITY);
|
||||
|
||||
/// Creates a new [`Size`] with the given width and height.
|
||||
///
|
||||
/// [`Size`]: struct.Size.html
|
||||
pub const fn new(width: f32, height: f32) -> Self {
|
||||
Size { width, height }
|
||||
}
|
||||
|
||||
/// Increments the [`Size`] to account for the given padding.
|
||||
///
|
||||
/// [`Size`]: struct.Size.html
|
||||
pub fn pad(&self, padding: f32) -> Self {
|
||||
pub fn pad(&self, padding: Padding) -> Self {
|
||||
Size {
|
||||
width: self.width + padding * 2.0,
|
||||
height: self.height + padding * 2.0,
|
||||
width: self.width + padding.horizontal() as f32,
|
||||
height: self.height + padding.vertical() as f32,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -49,3 +47,24 @@ impl From<[u16; 2]> for Size {
|
||||
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,20 +2,14 @@
|
||||
#[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 const fn new(x: T, y: T) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
@ -43,6 +37,17 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@ -54,3 +59,18 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 |
@ -50,6 +50,27 @@ We have not yet implemented a `LocalStorage` version of the auto-save feature. T
|
||||
|
||||
[TodoMVC]: http://todomvc.com/
|
||||
|
||||
## [Game of Life](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 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.
|
||||
|
||||
@ -69,8 +90,9 @@ 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 [`lyon`].
|
||||
- [`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.
|
||||
@ -78,8 +100,10 @@ A bunch of simpler examples exist:
|
||||
- [`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.
|
||||
@ -94,7 +118,7 @@ cargo run --package <example>
|
||||
[Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg
|
||||
|
||||
## [Coffee]
|
||||
Since [Iced was born in May], it has been powering the user interfaces in
|
||||
Since [Iced was born in May 2019], it has been powering the user interfaces in
|
||||
[Coffee], an experimental 2D game engine.
|
||||
|
||||
|
||||
@ -104,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
|
||||
|
@ -6,7 +6,4 @@ edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../.." }
|
||||
iced_native = { path = "../../native" }
|
||||
iced_wgpu = { path = "../../wgpu" }
|
||||
lyon = "0.15"
|
||||
iced = { path = "../..", features = ["canvas"] }
|
||||
|
@ -1,6 +1,6 @@
|
||||
## Bézier tool
|
||||
|
||||
A Paint-like tool for drawing Bézier curves using [`lyon`].
|
||||
A Paint-like tool for drawing Bézier curves using the `Canvas` widget.
|
||||
|
||||
The __[`main`]__ file contains all the code of the example.
|
||||
|
||||
@ -16,4 +16,3 @@ cargo run --package bezier_tool
|
||||
```
|
||||
|
||||
[`main`]: src/main.rs
|
||||
[`lyon`]: https://github.com/nical/lyon
|
||||
|
@ -1,294 +1,13 @@
|
||||
//! This example showcases a simple native custom widget that renders arbitrary
|
||||
//! path with `lyon`.
|
||||
mod bezier {
|
||||
// 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_native::{
|
||||
input, layout, Clipboard, Color, Element, Event, Font, Hasher,
|
||||
HorizontalAlignment, Layout, Length, MouseCursor, Point, Size, Vector,
|
||||
VerticalAlignment, Widget,
|
||||
};
|
||||
use iced_wgpu::{
|
||||
triangle::{Mesh2D, Vertex2D},
|
||||
Defaults, Primitive, Renderer,
|
||||
};
|
||||
use lyon::tessellation::{
|
||||
basic_shapes, BuffersBuilder, StrokeAttributes, StrokeOptions,
|
||||
StrokeTessellator, VertexBuffers,
|
||||
};
|
||||
|
||||
pub struct Bezier<'a, Message> {
|
||||
state: &'a mut State,
|
||||
curves: &'a [Curve],
|
||||
// [from, to, ctrl]
|
||||
on_click: Box<dyn Fn(Curve) -> Message>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Curve {
|
||||
from: Point,
|
||||
to: Point,
|
||||
control: Point,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
pending: Option<Pending>,
|
||||
}
|
||||
|
||||
enum Pending {
|
||||
One { from: Point },
|
||||
Two { from: Point, to: Point },
|
||||
}
|
||||
|
||||
impl<'a, Message> Bezier<'a, Message> {
|
||||
pub fn new<F>(
|
||||
state: &'a mut State,
|
||||
curves: &'a [Curve],
|
||||
on_click: F,
|
||||
) -> Self
|
||||
where
|
||||
F: 'static + Fn(Curve) -> Message,
|
||||
{
|
||||
Self {
|
||||
state,
|
||||
curves,
|
||||
on_click: Box::new(on_click),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> Widget<Message, Renderer> for Bezier<'a, Message> {
|
||||
fn width(&self) -> Length {
|
||||
Length::Fill
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Fill
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let size = limits
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill)
|
||||
.resolve(Size::ZERO);
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_renderer: &mut Renderer,
|
||||
defaults: &Defaults,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) -> (Primitive, MouseCursor) {
|
||||
let mut buffer: VertexBuffers<Vertex2D, u32> = VertexBuffers::new();
|
||||
let mut path_builder = lyon::path::Path::builder();
|
||||
|
||||
let bounds = layout.bounds();
|
||||
|
||||
// Draw rectangle border with lyon.
|
||||
basic_shapes::stroke_rectangle(
|
||||
&lyon::math::Rect::new(
|
||||
lyon::math::Point::new(0.5, 0.5),
|
||||
lyon::math::Size::new(
|
||||
bounds.width - 1.0,
|
||||
bounds.height - 1.0,
|
||||
),
|
||||
),
|
||||
&StrokeOptions::default().with_line_width(1.0),
|
||||
&mut BuffersBuilder::new(
|
||||
&mut buffer,
|
||||
|pos: lyon::math::Point, _: StrokeAttributes| Vertex2D {
|
||||
position: pos.to_array(),
|
||||
color: [0.0, 0.0, 0.0, 1.0],
|
||||
},
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for curve in self.curves {
|
||||
path_builder.move_to(lyon::math::Point::new(
|
||||
curve.from.x,
|
||||
curve.from.y,
|
||||
));
|
||||
|
||||
path_builder.quadratic_bezier_to(
|
||||
lyon::math::Point::new(curve.control.x, curve.control.y),
|
||||
lyon::math::Point::new(curve.to.x, curve.to.y),
|
||||
);
|
||||
}
|
||||
|
||||
match self.state.pending {
|
||||
None => {}
|
||||
Some(Pending::One { from }) => {
|
||||
path_builder
|
||||
.move_to(lyon::math::Point::new(from.x, from.y));
|
||||
path_builder.line_to(lyon::math::Point::new(
|
||||
cursor_position.x - bounds.x,
|
||||
cursor_position.y - bounds.y,
|
||||
));
|
||||
}
|
||||
Some(Pending::Two { from, to }) => {
|
||||
path_builder
|
||||
.move_to(lyon::math::Point::new(from.x, from.y));
|
||||
path_builder.quadratic_bezier_to(
|
||||
lyon::math::Point::new(
|
||||
cursor_position.x - bounds.x,
|
||||
cursor_position.y - bounds.y,
|
||||
),
|
||||
lyon::math::Point::new(to.x, to.y),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut tessellator = StrokeTessellator::new();
|
||||
|
||||
// Draw strokes with lyon.
|
||||
tessellator
|
||||
.tessellate(
|
||||
&path_builder.build(),
|
||||
&StrokeOptions::default().with_line_width(3.0),
|
||||
&mut BuffersBuilder::new(
|
||||
&mut buffer,
|
||||
|pos: lyon::math::Point, _: StrokeAttributes| {
|
||||
Vertex2D {
|
||||
position: pos.to_array(),
|
||||
color: [0.0, 0.0, 0.0, 1.0],
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mesh = Primitive::Mesh2D {
|
||||
origin: Point::new(bounds.x, bounds.y),
|
||||
buffers: Mesh2D {
|
||||
vertices: buffer.vertices,
|
||||
indices: buffer.indices,
|
||||
},
|
||||
};
|
||||
|
||||
(
|
||||
Primitive::Clip {
|
||||
bounds,
|
||||
offset: Vector::new(0, 0),
|
||||
content: Box::new(
|
||||
if self.curves.is_empty()
|
||||
&& self.state.pending.is_none()
|
||||
{
|
||||
let instructions = Primitive::Text {
|
||||
bounds,
|
||||
color: Color {
|
||||
a: defaults.text.color.a * 0.7,
|
||||
..defaults.text.color
|
||||
},
|
||||
content: String::from(
|
||||
"Click to create bezier curves!",
|
||||
),
|
||||
font: Font::Default,
|
||||
size: 30.0,
|
||||
horizontal_alignment:
|
||||
HorizontalAlignment::Center,
|
||||
vertical_alignment: VerticalAlignment::Center,
|
||||
};
|
||||
|
||||
Primitive::Group {
|
||||
primitives: vec![mesh, instructions],
|
||||
}
|
||||
} else {
|
||||
mesh
|
||||
},
|
||||
),
|
||||
},
|
||||
MouseCursor::OutOfBounds,
|
||||
)
|
||||
}
|
||||
|
||||
fn hash_layout(&self, _state: &mut Hasher) {}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
messages: &mut Vec<Message>,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: Option<&dyn Clipboard>,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if bounds.contains(cursor_position) {
|
||||
match event {
|
||||
Event::Mouse(input::mouse::Event::Input {
|
||||
state: input::ButtonState::Pressed,
|
||||
..
|
||||
}) => {
|
||||
let new_point = Point::new(
|
||||
cursor_position.x - bounds.x,
|
||||
cursor_position.y - bounds.y,
|
||||
);
|
||||
|
||||
match self.state.pending {
|
||||
None => {
|
||||
self.state.pending =
|
||||
Some(Pending::One { from: new_point });
|
||||
}
|
||||
Some(Pending::One { from }) => {
|
||||
self.state.pending = Some(Pending::Two {
|
||||
from,
|
||||
to: new_point,
|
||||
});
|
||||
}
|
||||
Some(Pending::Two { from, to }) => {
|
||||
self.state.pending = None;
|
||||
|
||||
messages.push((self.on_click)(Curve {
|
||||
from,
|
||||
to,
|
||||
control: new_point,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> Into<Element<'a, Message, Renderer>> for Bezier<'a, Message>
|
||||
where
|
||||
Message: 'static,
|
||||
{
|
||||
fn into(self) -> Element<'a, Message, Renderer> {
|
||||
Element::new(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use bezier::Bezier;
|
||||
//! This example showcases an interactive `Canvas` for drawing Bézier curves.
|
||||
use iced::{
|
||||
button, Align, Button, Column, Container, Element, Length, Sandbox,
|
||||
Settings, Text,
|
||||
button, Align, Button, Column, Element, Length, Sandbox, Settings, Text,
|
||||
};
|
||||
|
||||
pub fn main() {
|
||||
pub fn main() -> iced::Result {
|
||||
Example::run(Settings {
|
||||
antialiasing: true,
|
||||
..Settings::default()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@ -319,6 +38,7 @@ impl Sandbox for Example {
|
||||
match message {
|
||||
Message::AddCurve(curve) => {
|
||||
self.curves.push(curve);
|
||||
self.bezier.request_redraw();
|
||||
}
|
||||
Message::Clear => {
|
||||
self.bezier = bezier::State::default();
|
||||
@ -328,7 +48,7 @@ impl Sandbox for Example {
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let content = Column::new()
|
||||
Column::new()
|
||||
.padding(20)
|
||||
.spacing(20)
|
||||
.align_items(Align::Center)
|
||||
@ -337,22 +57,189 @@ impl Sandbox for Example {
|
||||
.width(Length::Shrink)
|
||||
.size(50),
|
||||
)
|
||||
.push(Bezier::new(
|
||||
&mut self.bezier,
|
||||
self.curves.as_slice(),
|
||||
Message::AddCurve,
|
||||
))
|
||||
.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),
|
||||
);
|
||||
|
||||
Container::new(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_x()
|
||||
.center_y()
|
||||
)
|
||||
.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,6 @@ authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
canvas = []
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["canvas", "async-std", "debug"] }
|
||||
iced_native = { path = "../../native" }
|
||||
iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
|
||||
chrono = "0.4"
|
||||
async-std = { version = "1.0", features = ["unstable"] }
|
||||
|
@ -1,9 +1,10 @@
|
||||
use iced::{
|
||||
canvas, executor, Application, Canvas, Color, Command, Container, Element,
|
||||
Length, Point, Settings, Subscription, Vector,
|
||||
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() {
|
||||
pub fn main() -> iced::Result {
|
||||
Clock::run(Settings {
|
||||
antialiasing: true,
|
||||
..Settings::default()
|
||||
@ -11,8 +12,8 @@ pub fn main() {
|
||||
}
|
||||
|
||||
struct Clock {
|
||||
now: LocalTime,
|
||||
clock: canvas::layer::Cache<LocalTime>,
|
||||
now: chrono::DateTime<chrono::Local>,
|
||||
clock: Cache,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@ -28,8 +29,8 @@ impl Application for Clock {
|
||||
fn new(_flags: ()) -> (Self, Command<Message>) {
|
||||
(
|
||||
Clock {
|
||||
now: chrono::Local::now().into(),
|
||||
clock: canvas::layer::Cache::new(),
|
||||
now: chrono::Local::now(),
|
||||
clock: Default::default(),
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
@ -39,10 +40,14 @@ impl Application for Clock {
|
||||
String::from("Clock - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Message,
|
||||
_clipboard: &mut Clipboard,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::Tick(local_time) => {
|
||||
let now = local_time.into();
|
||||
let now = local_time;
|
||||
|
||||
if now != self.now {
|
||||
self.now = now;
|
||||
@ -55,14 +60,14 @@ impl Application for Clock {
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
time::every(std::time::Duration::from_millis(500)).map(Message::Tick)
|
||||
time::every(std::time::Duration::from_millis(500))
|
||||
.map(|_| Message::Tick(chrono::Local::now()))
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let canvas = Canvas::new()
|
||||
let canvas = Canvas::new(self)
|
||||
.width(Length::Units(400))
|
||||
.height(Length::Units(400))
|
||||
.push(self.clock.with(&self.now));
|
||||
.height(Length::Units(400));
|
||||
|
||||
Container::new(canvas)
|
||||
.width(Length::Fill)
|
||||
@ -74,122 +79,59 @@ impl Application for Clock {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct LocalTime {
|
||||
hour: u32,
|
||||
minute: u32,
|
||||
second: u32,
|
||||
}
|
||||
|
||||
impl From<chrono::DateTime<chrono::Local>> for LocalTime {
|
||||
fn from(date_time: chrono::DateTime<chrono::Local>) -> LocalTime {
|
||||
impl canvas::Program<Message> for Clock {
|
||||
fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> {
|
||||
use chrono::Timelike;
|
||||
|
||||
LocalTime {
|
||||
hour: date_time.hour(),
|
||||
minute: date_time.minute(),
|
||||
second: date_time.second(),
|
||||
}
|
||||
}
|
||||
}
|
||||
let clock = self.clock.draw(bounds.size(), |frame| {
|
||||
let center = frame.center();
|
||||
let radius = frame.width().min(frame.height()) / 2.0;
|
||||
|
||||
impl canvas::Drawable for LocalTime {
|
||||
fn draw(&self, frame: &mut canvas::Frame) {
|
||||
let center = frame.center();
|
||||
let radius = frame.width().min(frame.height()) / 2.0;
|
||||
let offset = Vector::new(center.x, center.y);
|
||||
let background = Path::circle(center, radius);
|
||||
frame.fill(&background, Color::from_rgb8(0x12, 0x93, 0xD8));
|
||||
|
||||
let clock = canvas::Path::new(|path| path.circle(center, radius));
|
||||
let short_hand =
|
||||
Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius));
|
||||
|
||||
frame.fill(
|
||||
&clock,
|
||||
canvas::Fill::Color(Color::from_rgb8(0x12, 0x93, 0xD8)),
|
||||
);
|
||||
let long_hand =
|
||||
Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius));
|
||||
|
||||
fn draw_hand(
|
||||
n: u32,
|
||||
total: u32,
|
||||
length: f32,
|
||||
offset: Vector,
|
||||
path: &mut canvas::path::Builder,
|
||||
) {
|
||||
let turns = n as f32 / total as f32;
|
||||
let t = 2.0 * std::f32::consts::PI * (turns - 0.25);
|
||||
|
||||
let x = length * t.cos();
|
||||
let y = length * t.sin();
|
||||
|
||||
path.line_to(Point::new(x, y) + offset);
|
||||
}
|
||||
|
||||
let hour_and_minute_hands = canvas::Path::new(|path| {
|
||||
path.move_to(center);
|
||||
draw_hand(self.hour, 12, 0.5 * radius, offset, path);
|
||||
|
||||
path.move_to(center);
|
||||
draw_hand(self.minute, 60, 0.8 * radius, offset, path)
|
||||
});
|
||||
|
||||
frame.stroke(
|
||||
&hour_and_minute_hands,
|
||||
canvas::Stroke {
|
||||
width: radius / 100.0 * 3.0,
|
||||
color: Color::WHITE,
|
||||
line_cap: canvas::LineCap::Round,
|
||||
..canvas::Stroke::default()
|
||||
},
|
||||
);
|
||||
|
||||
let second_hand = canvas::Path::new(|path| {
|
||||
path.move_to(center);
|
||||
draw_hand(self.second, 60, 0.8 * radius, offset, path)
|
||||
});
|
||||
|
||||
frame.stroke(
|
||||
&second_hand,
|
||||
canvas::Stroke {
|
||||
let thin_stroke = Stroke {
|
||||
width: radius / 100.0,
|
||||
color: Color::WHITE,
|
||||
line_cap: canvas::LineCap::Round,
|
||||
..canvas::Stroke::default()
|
||||
},
|
||||
);
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
mod time {
|
||||
use iced::futures;
|
||||
fn hand_rotation(n: u32, total: u32) -> f32 {
|
||||
let turns = n as f32 / total as f32;
|
||||
|
||||
pub fn every(
|
||||
duration: std::time::Duration,
|
||||
) -> iced::Subscription<chrono::DateTime<chrono::Local>> {
|
||||
iced::Subscription::from_recipe(Every(duration))
|
||||
}
|
||||
|
||||
struct Every(std::time::Duration);
|
||||
|
||||
impl<H, I> iced_native::subscription::Recipe<H, I> for Every
|
||||
where
|
||||
H: std::hash::Hasher,
|
||||
{
|
||||
type Output = chrono::DateTime<chrono::Local>;
|
||||
|
||||
fn hash(&self, state: &mut H) {
|
||||
use std::hash::Hash;
|
||||
|
||||
std::any::TypeId::of::<Self>().hash(state);
|
||||
self.0.hash(state);
|
||||
}
|
||||
|
||||
fn stream(
|
||||
self: Box<Self>,
|
||||
_input: futures::stream::BoxStream<'static, I>,
|
||||
) -> futures::stream::BoxStream<'static, Self::Output> {
|
||||
use futures::stream::StreamExt;
|
||||
|
||||
async_std::stream::interval(self.0)
|
||||
.map(|_| chrono::Local::now())
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
use iced::{button, Align, Button, Column, Element, Sandbox, Settings, Text};
|
||||
|
||||
pub fn main() {
|
||||
pub fn main() -> iced::Result {
|
||||
Counter::run(Settings::default())
|
||||
}
|
||||
|
||||
|
@ -8,4 +8,4 @@ publish = false
|
||||
[dependencies]
|
||||
iced = { path = "../.." }
|
||||
iced_native = { path = "../../native" }
|
||||
iced_wgpu = { path = "../../wgpu" }
|
||||
iced_graphics = { path = "../../graphics" }
|
||||
|
@ -9,23 +9,26 @@ mod circle {
|
||||
// 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, Background, Color, Element, Hasher, Layout, Length,
|
||||
MouseCursor, Point, Size, Widget,
|
||||
layout, mouse, Background, Color, Element, Hasher, Layout, Length,
|
||||
Point, Rectangle, Size, Widget,
|
||||
};
|
||||
use iced_wgpu::{Defaults, Primitive, Renderer};
|
||||
|
||||
pub struct Circle {
|
||||
radius: u16,
|
||||
radius: f32,
|
||||
}
|
||||
|
||||
impl Circle {
|
||||
pub fn new(radius: u16) -> Self {
|
||||
pub fn new(radius: f32) -> Self {
|
||||
Self { radius }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message> Widget<Message, Renderer> for Circle {
|
||||
impl<Message, B> Widget<Message, Renderer<B>> for Circle
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
Length::Shrink
|
||||
}
|
||||
@ -36,43 +39,44 @@ mod circle {
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
_renderer: &Renderer<B>,
|
||||
_limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout::Node::new(Size::new(
|
||||
f32::from(self.radius) * 2.0,
|
||||
f32::from(self.radius) * 2.0,
|
||||
))
|
||||
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.hash(state);
|
||||
self.radius.to_bits().hash(state);
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_renderer: &mut Renderer,
|
||||
_renderer: &mut Renderer<B>,
|
||||
_defaults: &Defaults,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
) -> (Primitive, MouseCursor) {
|
||||
_viewport: &Rectangle,
|
||||
) -> (Primitive, mouse::Interaction) {
|
||||
(
|
||||
Primitive::Quad {
|
||||
bounds: layout.bounds(),
|
||||
background: Background::Color(Color::BLACK),
|
||||
border_radius: self.radius,
|
||||
border_width: 0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
MouseCursor::OutOfBounds,
|
||||
mouse::Interaction::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> Into<Element<'a, Message, Renderer>> for Circle {
|
||||
fn into(self) -> Element<'a, Message, Renderer> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -84,12 +88,12 @@ use iced::{
|
||||
Slider, Text,
|
||||
};
|
||||
|
||||
pub fn main() {
|
||||
pub fn main() -> iced::Result {
|
||||
Example::run(Settings::default())
|
||||
}
|
||||
|
||||
struct Example {
|
||||
radius: u16,
|
||||
radius: f32,
|
||||
slider: slider::State,
|
||||
}
|
||||
|
||||
@ -103,7 +107,7 @@ impl Sandbox for Example {
|
||||
|
||||
fn new() -> Self {
|
||||
Example {
|
||||
radius: 50,
|
||||
radius: 50.0,
|
||||
slider: slider::State::new(),
|
||||
}
|
||||
}
|
||||
@ -115,7 +119,7 @@ impl Sandbox for Example {
|
||||
fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::RadiusChanged(radius) => {
|
||||
self.radius = radius.round() as u16;
|
||||
self.radius = radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -127,13 +131,16 @@ impl Sandbox for Example {
|
||||
.max_width(500)
|
||||
.align_items(Align::Center)
|
||||
.push(Circle::new(self.radius))
|
||||
.push(Text::new(format!("Radius: {}", self.radius.to_string())))
|
||||
.push(Slider::new(
|
||||
&mut self.slider,
|
||||
1.0..=100.0,
|
||||
f32::from(self.radius),
|
||||
Message::RadiusChanged,
|
||||
));
|
||||
.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)
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "download_progress"
|
||||
version = "0.1.0"
|
||||
authors = ["Songtronix <contact@songtronix.com>"]
|
||||
authors = ["Songtronix <contact@songtronix.com>", "Folyd <lyshuhow@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
@ -9,4 +9,4 @@ publish = false
|
||||
iced = { path = "../..", features = ["tokio"] }
|
||||
iced_native = { path = "../../native" }
|
||||
iced_futures = { path = "../../futures" }
|
||||
reqwest = "0.10"
|
||||
reqwest = "0.11"
|
||||
|
@ -1,6 +1,6 @@
|
||||
## Download progress
|
||||
|
||||
A basic application that asynchronously downloads a dummy file of 100 MB and tracks the 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.
|
||||
|
||||
|
@ -1,37 +1,46 @@
|
||||
use iced_futures::futures;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
// Just a little utility function
|
||||
pub fn file<T: ToString>(url: T) -> iced::Subscription<Progress> {
|
||||
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 {
|
||||
pub struct Download<I> {
|
||||
id: I,
|
||||
url: String,
|
||||
}
|
||||
|
||||
// Make sure iced can use our download stream
|
||||
impl<H, I> iced_native::subscription::Recipe<H, I> for Download
|
||||
impl<H, I, T> iced_native::subscription::Recipe<H, I> for Download<T>
|
||||
where
|
||||
H: std::hash::Hasher,
|
||||
T: 'static + Hash + Copy + Send,
|
||||
H: Hasher,
|
||||
{
|
||||
type Output = Progress;
|
||||
type Output = (T, Progress);
|
||||
|
||||
fn hash(&self, state: &mut H) {
|
||||
use std::hash::Hash;
|
||||
struct Marker;
|
||||
std::any::TypeId::of::<Marker>().hash(state);
|
||||
|
||||
std::any::TypeId::of::<Self>().hash(state);
|
||||
self.url.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),
|
||||
|state| async move {
|
||||
move |state| async move {
|
||||
match state {
|
||||
State::Ready(url) => {
|
||||
let response = reqwest::get(&url).await;
|
||||
@ -40,7 +49,7 @@ where
|
||||
Ok(response) => {
|
||||
if let Some(total) = response.content_length() {
|
||||
Some((
|
||||
Progress::Started,
|
||||
(id, Progress::Started),
|
||||
State::Downloading {
|
||||
response,
|
||||
total,
|
||||
@ -48,11 +57,14 @@ where
|
||||
},
|
||||
))
|
||||
} else {
|
||||
Some((Progress::Errored, State::Finished))
|
||||
Some((
|
||||
(id, Progress::Errored),
|
||||
State::Finished,
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
Some((Progress::Errored, State::Finished))
|
||||
Some(((id, Progress::Errored), State::Finished))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -68,7 +80,7 @@ where
|
||||
(downloaded as f32 / total as f32) * 100.0;
|
||||
|
||||
Some((
|
||||
Progress::Advanced(percentage),
|
||||
(id, Progress::Advanced(percentage)),
|
||||
State::Downloading {
|
||||
response,
|
||||
total,
|
||||
@ -76,8 +88,12 @@ where
|
||||
},
|
||||
))
|
||||
}
|
||||
Ok(None) => Some((Progress::Finished, State::Finished)),
|
||||
Err(_) => Some((Progress::Errored, State::Finished)),
|
||||
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
|
||||
|
@ -1,26 +1,26 @@
|
||||
use iced::{
|
||||
button, executor, Align, Application, Button, Column, Command, Container,
|
||||
Element, Length, ProgressBar, Settings, Subscription, Text,
|
||||
button, executor, Align, Application, Button, Clipboard, Column, Command,
|
||||
Container, Element, Length, ProgressBar, Settings, Subscription, Text,
|
||||
};
|
||||
|
||||
mod download;
|
||||
|
||||
pub fn main() {
|
||||
pub fn main() -> iced::Result {
|
||||
Example::run(Settings::default())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Example {
|
||||
Idle { button: button::State },
|
||||
Downloading { progress: f32 },
|
||||
Finished { button: button::State },
|
||||
Errored { button: button::State },
|
||||
struct Example {
|
||||
downloads: Vec<Download>,
|
||||
last_id: usize,
|
||||
add: button::State,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
Download,
|
||||
DownloadProgressed(download::Progress),
|
||||
Add,
|
||||
Download(usize),
|
||||
DownloadProgressed((usize, download::Progress)),
|
||||
}
|
||||
|
||||
impl Application for Example {
|
||||
@ -30,8 +30,10 @@ impl Application for Example {
|
||||
|
||||
fn new(_flags: ()) -> (Example, Command<Message>) {
|
||||
(
|
||||
Example::Idle {
|
||||
button: button::State::new(),
|
||||
Example {
|
||||
downloads: vec![Download::new(0)],
|
||||
last_id: 0,
|
||||
add: button::State::new(),
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
@ -41,104 +43,177 @@ impl Application for Example {
|
||||
String::from("Download progress - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Message,
|
||||
_clipboard: &mut Clipboard,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::Download => match self {
|
||||
Example::Idle { .. }
|
||||
| Example::Finished { .. }
|
||||
| Example::Errored { .. } => {
|
||||
*self = Example::Downloading { progress: 0.0 };
|
||||
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(message) => match self {
|
||||
Example::Downloading { progress } => match message {
|
||||
download::Progress::Started => {
|
||||
*progress = 0.0;
|
||||
}
|
||||
download::Progress::Advanced(percentage) => {
|
||||
*progress = percentage;
|
||||
}
|
||||
download::Progress::Finished => {
|
||||
*self = Example::Finished {
|
||||
button: button::State::new(),
|
||||
}
|
||||
}
|
||||
download::Progress::Errored => {
|
||||
*self = Example::Errored {
|
||||
button: button::State::new(),
|
||||
};
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
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> {
|
||||
match self {
|
||||
Example::Downloading { .. } => {
|
||||
download::file("https://speed.hetzner.de/100MB.bin")
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let current_progress = match self {
|
||||
Example::Idle { .. } => 0.0,
|
||||
Example::Downloading { progress } => *progress,
|
||||
Example::Finished { .. } => 100.0,
|
||||
Example::Errored { .. } => 0.0,
|
||||
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 self {
|
||||
Example::Idle { button } => {
|
||||
let control: Element<_> = match &mut self.state {
|
||||
State::Idle { button } => {
|
||||
Button::new(button, Text::new("Start the download!"))
|
||||
.on_press(Message::Download)
|
||||
.on_press(Message::Download(self.id))
|
||||
.into()
|
||||
}
|
||||
Example::Finished { button } => Column::new()
|
||||
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),
|
||||
.on_press(Message::Download(self.id)),
|
||||
)
|
||||
.into(),
|
||||
Example::Downloading { .. } => {
|
||||
State::Downloading { .. } => {
|
||||
Text::new(format!("Downloading... {:.2}%", current_progress))
|
||||
.into()
|
||||
}
|
||||
Example::Errored { button } => Column::new()
|
||||
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),
|
||||
.on_press(Message::Download(self.id)),
|
||||
)
|
||||
.into(),
|
||||
};
|
||||
|
||||
let content = Column::new()
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.padding(10)
|
||||
.align_items(Align::Center)
|
||||
.push(progress_bar)
|
||||
.push(control);
|
||||
|
||||
Container::new(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_x()
|
||||
.center_y()
|
||||
.push(control)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,30 @@
|
||||
use iced::{
|
||||
executor, Align, Application, Checkbox, Column, Command, Container,
|
||||
Element, Length, Settings, Subscription, Text,
|
||||
button, executor, Align, Application, Button, Checkbox, Clipboard, Column,
|
||||
Command, Container, Element, HorizontalAlignment, Length, Settings,
|
||||
Subscription, Text,
|
||||
};
|
||||
use iced_native::{window, Event};
|
||||
|
||||
pub fn main() {
|
||||
Events::run(Settings::default())
|
||||
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 {
|
||||
@ -32,29 +40,41 @@ impl Application for Events {
|
||||
String::from("Events - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Message,
|
||||
_clipboard: &mut Clipboard,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::EventOccurred(event) => {
|
||||
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> {
|
||||
if self.enabled {
|
||||
iced_native::subscription::events().map(Message::EventOccurred)
|
||||
} else {
|
||||
Subscription::none()
|
||||
}
|
||||
iced_native::subscription::events().map(Message::EventOccurred)
|
||||
}
|
||||
|
||||
fn should_exit(&self) -> bool {
|
||||
self.should_exit
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
@ -71,11 +91,22 @@ impl Application for 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(toggle)
|
||||
.push(exit);
|
||||
|
||||
Container::new(content)
|
||||
.width(Length::Fill)
|
||||
|
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
|
||||
}
|
||||
}
|
||||
}
|
@ -8,4 +8,4 @@ publish = false
|
||||
[dependencies]
|
||||
iced = { path = "../.." }
|
||||
iced_native = { path = "../../native" }
|
||||
iced_wgpu = { path = "../../wgpu" }
|
||||
iced_graphics = { path = "../../graphics" }
|
||||
|
@ -10,13 +10,13 @@ mod rainbow {
|
||||
// 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_native::{
|
||||
layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size,
|
||||
Widget,
|
||||
};
|
||||
use iced_wgpu::{
|
||||
use iced_graphics::{
|
||||
triangle::{Mesh2D, Vertex2D},
|
||||
Defaults, Primitive, Renderer,
|
||||
Backend, Defaults, Primitive, Renderer,
|
||||
};
|
||||
use iced_native::{
|
||||
layout, mouse, Element, Hasher, Layout, Length, Point, Rectangle, Size,
|
||||
Vector, Widget,
|
||||
};
|
||||
|
||||
pub struct Rainbow;
|
||||
@ -27,7 +27,10 @@ mod rainbow {
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message> Widget<Message, Renderer> for Rainbow {
|
||||
impl<Message, B> Widget<Message, Renderer<B>> for Rainbow
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn width(&self) -> Length {
|
||||
Length::Fill
|
||||
}
|
||||
@ -38,7 +41,7 @@ mod rainbow {
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
_renderer: &Renderer<B>,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let size = limits.width(Length::Fill).resolve(Size::ZERO);
|
||||
@ -50,11 +53,12 @@ mod rainbow {
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_renderer: &mut Renderer,
|
||||
_renderer: &mut Renderer<B>,
|
||||
_defaults: &Defaults,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) -> (Primitive, MouseCursor) {
|
||||
_viewport: &Rectangle,
|
||||
) -> (Primitive, mouse::Interaction) {
|
||||
let b = layout.bounds();
|
||||
|
||||
// R O Y G B I V
|
||||
@ -85,66 +89,72 @@ mod rainbow {
|
||||
let posn_l = [0.0, b.height / 2.0];
|
||||
|
||||
(
|
||||
Primitive::Mesh2D {
|
||||
origin: Point::new(b.x, b.y),
|
||||
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
|
||||
],
|
||||
},
|
||||
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
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
MouseCursor::OutOfBounds,
|
||||
mouse::Interaction::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> Into<Element<'a, Message, Renderer>> for Rainbow {
|
||||
fn into(self) -> Element<'a, Message, Renderer> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -156,7 +166,7 @@ use iced::{
|
||||
};
|
||||
use rainbow::Rainbow;
|
||||
|
||||
pub fn main() {
|
||||
pub fn main() -> iced::Result {
|
||||
Example::run(Settings::default())
|
||||
}
|
||||
|
||||
|
@ -8,4 +8,4 @@ publish = false
|
||||
[dependencies]
|
||||
iced_winit = { path = "../../winit" }
|
||||
iced_wgpu = { path = "../../wgpu" }
|
||||
env_logger = "0.7"
|
||||
env_logger = "0.8"
|
||||
|
@ -1,15 +1,15 @@
|
||||
use crate::Scene;
|
||||
|
||||
use iced_wgpu::Renderer;
|
||||
use iced_winit::{
|
||||
slider, Align, Color, Column, Element, Length, Row, Slider, Text,
|
||||
slider, Align, Clipboard, Color, Column, Command, Element, Length, Program,
|
||||
Row, Slider, Text,
|
||||
};
|
||||
|
||||
pub struct Controls {
|
||||
background_color: Color,
|
||||
sliders: [slider::State; 3],
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
BackgroundColorChanged(Color),
|
||||
}
|
||||
@ -17,58 +17,69 @@ pub enum Message {
|
||||
impl Controls {
|
||||
pub fn new() -> Controls {
|
||||
Controls {
|
||||
background_color: Color::BLACK,
|
||||
sliders: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&self, message: Message, scene: &mut Scene) {
|
||||
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) => {
|
||||
scene.background_color = color;
|
||||
self.background_color = color;
|
||||
}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
pub fn view(&mut self, scene: &Scene) -> Element<Message, Renderer> {
|
||||
fn view(&mut self) -> Element<Message, Renderer> {
|
||||
let [r, g, b] = &mut self.sliders;
|
||||
let background_color = scene.background_color;
|
||||
let background_color = self.background_color;
|
||||
|
||||
let sliders = Row::new()
|
||||
.width(Length::Units(500))
|
||||
.spacing(20)
|
||||
.push(Slider::new(
|
||||
r,
|
||||
0.0..=1.0,
|
||||
scene.background_color.r,
|
||||
move |r| {
|
||||
.push(
|
||||
Slider::new(r, 0.0..=1.0, background_color.r, move |r| {
|
||||
Message::BackgroundColorChanged(Color {
|
||||
r,
|
||||
..background_color
|
||||
})
|
||||
},
|
||||
))
|
||||
.push(Slider::new(
|
||||
g,
|
||||
0.0..=1.0,
|
||||
scene.background_color.g,
|
||||
move |g| {
|
||||
})
|
||||
.step(0.01),
|
||||
)
|
||||
.push(
|
||||
Slider::new(g, 0.0..=1.0, background_color.g, move |g| {
|
||||
Message::BackgroundColorChanged(Color {
|
||||
g,
|
||||
..background_color
|
||||
})
|
||||
},
|
||||
))
|
||||
.push(Slider::new(
|
||||
b,
|
||||
0.0..=1.0,
|
||||
scene.background_color.b,
|
||||
move |b| {
|
||||
})
|
||||
.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)
|
||||
|
@ -4,12 +4,12 @@ mod scene;
|
||||
use controls::Controls;
|
||||
use scene::Scene;
|
||||
|
||||
use iced_wgpu::{
|
||||
wgpu, window::SwapChain, Primitive, Renderer, Settings, Target,
|
||||
};
|
||||
use iced_winit::{winit, Cache, Clipboard, MouseCursor, Size, UserInterface};
|
||||
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},
|
||||
};
|
||||
@ -20,45 +20,80 @@ pub fn main() {
|
||||
// Initialize winit
|
||||
let event_loop = EventLoop::new();
|
||||
let window = winit::window::Window::new(&event_loop).unwrap();
|
||||
let mut logical_size =
|
||||
window.inner_size().to_logical(window.scale_factor());
|
||||
|
||||
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 adapter = wgpu::Adapter::request(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::Default,
|
||||
backends: wgpu::BackendBit::PRIMARY,
|
||||
})
|
||||
.expect("Request adapter");
|
||||
// Initialize wgpu
|
||||
let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY);
|
||||
let surface = unsafe { instance.create_surface(&window) };
|
||||
|
||||
let (mut device, mut queue) =
|
||||
adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
extensions: wgpu::Extensions {
|
||||
anisotropic_filtering: false,
|
||||
},
|
||||
limits: wgpu::Limits::default(),
|
||||
});
|
||||
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 surface = wgpu::Surface::create(&window);
|
||||
let format = wgpu::TextureFormat::Bgra8UnormSrgb;
|
||||
|
||||
let mut swap_chain = {
|
||||
let size = window.inner_size();
|
||||
|
||||
SwapChain::new(&device, &surface, format, size.width, size.height)
|
||||
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 iced
|
||||
let mut events = Vec::new();
|
||||
let mut cache = Some(Cache::default());
|
||||
let mut renderer = Renderer::new(&mut device, Settings::default());
|
||||
let mut output = (Primitive::None, MouseCursor::OutOfBounds);
|
||||
let clipboard = Clipboard::new(&window);
|
||||
// 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 mut scene = Scene::new(&device);
|
||||
let mut controls = Controls::new();
|
||||
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| {
|
||||
@ -68,18 +103,23 @@ pub fn main() {
|
||||
match event {
|
||||
Event::WindowEvent { event, .. } => {
|
||||
match event {
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
cursor_position = position;
|
||||
}
|
||||
WindowEvent::ModifiersChanged(new_modifiers) => {
|
||||
modifiers = new_modifiers;
|
||||
}
|
||||
WindowEvent::Resized(new_size) => {
|
||||
logical_size =
|
||||
new_size.to_logical(window.scale_factor());
|
||||
viewport = Viewport::with_physical_size(
|
||||
Size::new(new_size.width, new_size.height),
|
||||
window.scale_factor(),
|
||||
);
|
||||
|
||||
resized = true;
|
||||
}
|
||||
WindowEvent::CloseRequested => {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@ -89,114 +129,95 @@ pub fn main() {
|
||||
window.scale_factor(),
|
||||
modifiers,
|
||||
) {
|
||||
events.push(event);
|
||||
state.queue_event(event);
|
||||
}
|
||||
}
|
||||
Event::MainEventsCleared => {
|
||||
// If no relevant events happened, we can simply skip this
|
||||
if events.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to:
|
||||
// 1. Process events of our user interface.
|
||||
// 2. Update state as a result of any interaction.
|
||||
// 3. Generate a new output for our renderer.
|
||||
|
||||
// First, we build our user interface.
|
||||
let mut user_interface = UserInterface::build(
|
||||
controls.view(&scene),
|
||||
Size::new(logical_size.width, logical_size.height),
|
||||
cache.take().unwrap(),
|
||||
&mut renderer,
|
||||
);
|
||||
|
||||
// Then, we process the events, obtaining messages in return.
|
||||
let messages = user_interface.update(
|
||||
events.drain(..),
|
||||
clipboard.as_ref().map(|c| c as _),
|
||||
&renderer,
|
||||
);
|
||||
|
||||
let user_interface = if messages.is_empty() {
|
||||
// If there are no messages, no interactions we care about have
|
||||
// happened. We can simply leave our user interface as it is.
|
||||
user_interface
|
||||
} else {
|
||||
// If there are messages, we need to update our state
|
||||
// accordingly and rebuild our user interface.
|
||||
// We can only do this if we drop our user interface first
|
||||
// by turning it into its cache.
|
||||
cache = Some(user_interface.into_cache());
|
||||
|
||||
// In this example, `Controls` is the only part that cares
|
||||
// about messages, so updating our state is pretty
|
||||
// straightforward.
|
||||
for message in messages {
|
||||
controls.update(message, &mut scene);
|
||||
}
|
||||
|
||||
// Once the state has been changed, we rebuild our updated
|
||||
// user interface.
|
||||
UserInterface::build(
|
||||
controls.view(&scene),
|
||||
Size::new(logical_size.width, logical_size.height),
|
||||
cache.take().unwrap(),
|
||||
// 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,
|
||||
);
|
||||
|
||||
// Finally, we just need to draw a new output for our renderer,
|
||||
output = user_interface.draw(&mut renderer);
|
||||
|
||||
// update our cache,
|
||||
cache = Some(user_interface.into_cache());
|
||||
|
||||
// and request a redraw
|
||||
window.request_redraw();
|
||||
// and request a redraw
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
Event::RedrawRequested(_) => {
|
||||
if resized {
|
||||
let size = window.inner_size();
|
||||
|
||||
swap_chain = SwapChain::new(
|
||||
&device,
|
||||
swap_chain = device.create_swap_chain(
|
||||
&surface,
|
||||
format,
|
||||
size.width,
|
||||
size.height,
|
||||
&wgpu::SwapChainDescriptor {
|
||||
usage: wgpu::TextureUsage::RENDER_ATTACHMENT,
|
||||
format: format,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: wgpu::PresentMode::Mailbox,
|
||||
},
|
||||
);
|
||||
|
||||
resized = false;
|
||||
}
|
||||
|
||||
let (frame, viewport) = swap_chain.next_frame();
|
||||
let frame = swap_chain.get_current_frame().expect("Next frame");
|
||||
|
||||
let mut encoder = device.create_command_encoder(
|
||||
&wgpu::CommandEncoderDescriptor { todo: 0 },
|
||||
&wgpu::CommandEncoderDescriptor { label: None },
|
||||
);
|
||||
|
||||
// We draw the scene first
|
||||
scene.draw(&mut encoder, &frame.view);
|
||||
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_cursor = renderer.draw(
|
||||
let mouse_interaction = renderer.backend_mut().draw(
|
||||
&mut device,
|
||||
&mut staging_belt,
|
||||
&mut encoder,
|
||||
Target {
|
||||
texture: &frame.view,
|
||||
viewport,
|
||||
},
|
||||
&output,
|
||||
window.scale_factor(),
|
||||
&["Some debug information!"],
|
||||
&frame.output.view,
|
||||
&viewport,
|
||||
state.primitive(),
|
||||
&debug.overlay(),
|
||||
);
|
||||
|
||||
// Then we submit the work
|
||||
queue.submit(&[encoder.finish()]);
|
||||
staging_belt.finish();
|
||||
queue.submit(Some(encoder.finish()));
|
||||
|
||||
// And update the mouse cursor
|
||||
window.set_cursor_icon(iced_winit::conversion::mouse_cursor(
|
||||
mouse_cursor,
|
||||
));
|
||||
// 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();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
@ -2,118 +2,98 @@ use iced_wgpu::wgpu;
|
||||
use iced_winit::Color;
|
||||
|
||||
pub struct Scene {
|
||||
pub background_color: Color,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
bind_group: wgpu::BindGroup,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
pub fn new(device: &wgpu::Device) -> Scene {
|
||||
let (pipeline, bind_group) = build_pipeline(device);
|
||||
let pipeline = build_pipeline(device);
|
||||
|
||||
Scene {
|
||||
background_color: Color::BLACK,
|
||||
pipeline,
|
||||
bind_group,
|
||||
}
|
||||
Scene { pipeline }
|
||||
}
|
||||
|
||||
pub fn draw(
|
||||
pub fn clear<'a>(
|
||||
&self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
target: &wgpu::TextureView,
|
||||
) {
|
||||
let mut rpass =
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[
|
||||
wgpu::RenderPassColorAttachmentDescriptor {
|
||||
attachment: target,
|
||||
resolve_target: None,
|
||||
load_op: wgpu::LoadOp::Clear,
|
||||
store_op: wgpu::StoreOp::Store,
|
||||
clear_color: {
|
||||
let [r, g, b, a] =
|
||||
self.background_color.into_linear();
|
||||
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,
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
depth_stencil_attachment: None,
|
||||
});
|
||||
wgpu::Color {
|
||||
r: r as f64,
|
||||
g: g as f64,
|
||||
b: b as f64,
|
||||
a: a as f64,
|
||||
}
|
||||
}),
|
||||
store: true,
|
||||
},
|
||||
}],
|
||||
depth_stencil_attachment: None,
|
||||
})
|
||||
}
|
||||
|
||||
rpass.set_pipeline(&self.pipeline);
|
||||
rpass.set_bind_group(0, &self.bind_group, &[]);
|
||||
rpass.draw(0..3, 0..1);
|
||||
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, wgpu::BindGroup) {
|
||||
let vs = include_bytes!("shader/vert.spv");
|
||||
let fs = include_bytes!("shader/frag.spv");
|
||||
fn build_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline {
|
||||
let vs_module =
|
||||
device.create_shader_module(&wgpu::include_spirv!("shader/vert.spv"));
|
||||
|
||||
let vs_module = device.create_shader_module(
|
||||
&wgpu::read_spirv(std::io::Cursor::new(&vs[..])).unwrap(),
|
||||
);
|
||||
|
||||
let fs_module = device.create_shader_module(
|
||||
&wgpu::read_spirv(std::io::Cursor::new(&fs[..])).unwrap(),
|
||||
);
|
||||
|
||||
let bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
bindings: &[],
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
layout: &bind_group_layout,
|
||||
bindings: &[],
|
||||
});
|
||||
let fs_module =
|
||||
device.create_shader_module(&wgpu::include_spirv!("shader/frag.spv"));
|
||||
|
||||
let pipeline_layout =
|
||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
bind_group_layouts: &[&bind_group_layout],
|
||||
label: None,
|
||||
push_constant_ranges: &[],
|
||||
bind_group_layouts: &[],
|
||||
});
|
||||
|
||||
let pipeline =
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
layout: &pipeline_layout,
|
||||
vertex_stage: wgpu::ProgrammableStageDescriptor {
|
||||
label: None,
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &vs_module,
|
||||
entry_point: "main",
|
||||
buffers: &[],
|
||||
},
|
||||
fragment_stage: Some(wgpu::ProgrammableStageDescriptor {
|
||||
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,
|
||||
}],
|
||||
}),
|
||||
rasterization_state: Some(wgpu::RasterizationStateDescriptor {
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: wgpu::CullMode::None,
|
||||
depth_bias: 0,
|
||||
depth_bias_slope_scale: 0.0,
|
||||
depth_bias_clamp: 0.0,
|
||||
}),
|
||||
primitive_topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
color_states: &[wgpu::ColorStateDescriptor {
|
||||
format: wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||
color_blend: wgpu::BlendDescriptor::REPLACE,
|
||||
alpha_blend: wgpu::BlendDescriptor::REPLACE,
|
||||
write_mask: wgpu::ColorWrite::ALL,
|
||||
}],
|
||||
depth_stencil_state: None,
|
||||
index_format: wgpu::IndexFormat::Uint16,
|
||||
vertex_buffers: &[],
|
||||
sample_count: 1,
|
||||
sample_mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
(pipeline, bind_group)
|
||||
pipeline
|
||||
}
|
||||
|
@ -6,4 +6,5 @@ edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../.." }
|
||||
iced = { path = "../..", features = ["debug"] }
|
||||
iced_native = { path = "../../native" }
|
||||
|
@ -15,8 +15,8 @@ This example showcases the `PaneGrid` widget, which features:
|
||||
The __[`main`]__ file contains all the code of the example.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/mixedflatjellyfish">
|
||||
<img src="https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif">
|
||||
<a href="https://gfycat.com/frailfreshairedaleterrier">
|
||||
<img src="https://thumbs.gfycat.com/FrailFreshAiredaleterrier-small.gif">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
@ -1,16 +1,19 @@
|
||||
use iced::{
|
||||
button, keyboard, pane_grid, scrollable, Align, Button, Column, Container,
|
||||
Element, HorizontalAlignment, Length, PaneGrid, Sandbox, Scrollable,
|
||||
Settings, Text,
|
||||
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() {
|
||||
pub fn main() -> iced::Result {
|
||||
Example::run(Settings::default())
|
||||
}
|
||||
|
||||
struct Example {
|
||||
panes: pane_grid::State<Content>,
|
||||
panes: pane_grid::State<Pane>,
|
||||
panes_created: usize,
|
||||
focus: Option<pane_grid::Pane>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@ -18,59 +21,82 @@ 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 Sandbox for Example {
|
||||
impl Application for Example {
|
||||
type Message = Message;
|
||||
type Executor = executor::Default;
|
||||
type Flags = ();
|
||||
|
||||
fn new() -> Self {
|
||||
let (panes, _) = pane_grid::State::new(Content::new(0));
|
||||
fn new(_flags: ()) -> (Self, Command<Message>) {
|
||||
let (panes, _) = pane_grid::State::new(Pane::new(0));
|
||||
|
||||
Example {
|
||||
panes,
|
||||
panes_created: 1,
|
||||
}
|
||||
(
|
||||
Example {
|
||||
panes,
|
||||
panes_created: 1,
|
||||
focus: None,
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Pane grid - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) {
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Message,
|
||||
_clipboard: &mut Clipboard,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::Split(axis, pane) => {
|
||||
let _ = self.panes.split(
|
||||
let result = self.panes.split(
|
||||
axis,
|
||||
&pane,
|
||||
Content::new(self.panes_created),
|
||||
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.panes.active() {
|
||||
let _ = self.panes.split(
|
||||
if let Some(pane) = self.focus {
|
||||
let result = self.panes.split(
|
||||
axis,
|
||||
&pane,
|
||||
Content::new(self.panes_created),
|
||||
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.panes.active() {
|
||||
if let Some(pane) = self.focus {
|
||||
if let Some(adjacent) =
|
||||
self.panes.adjacent(&pane, direction)
|
||||
{
|
||||
self.panes.focus(&adjacent);
|
||||
self.focus = Some(adjacent);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Clicked(pane) => {
|
||||
self.focus = Some(pane);
|
||||
}
|
||||
Message::Resized(pane_grid::ResizeEvent { split, ratio }) => {
|
||||
self.panes.resize(&split, ratio);
|
||||
}
|
||||
@ -81,30 +107,97 @@ impl Sandbox for Example {
|
||||
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) => {
|
||||
let _ = self.panes.close(&pane);
|
||||
if let Some((_, sibling)) = self.panes.close(&pane) {
|
||||
self.focus = Some(sibling);
|
||||
}
|
||||
}
|
||||
Message::CloseFocused => {
|
||||
if let Some(pane) = self.panes.active() {
|
||||
let _ = self.panes.close(&pane);
|
||||
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, |pane, content, focus| {
|
||||
content.view(pane, focus, total_panes)
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.spacing(10)
|
||||
.on_drag(Message::Dragged)
|
||||
.on_resize(Message::Resized)
|
||||
.on_key_press(handle_hotkey);
|
||||
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)
|
||||
@ -114,11 +207,22 @@ impl Sandbox for Example {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_hotkey(event: pane_grid::KeyPressEvent) -> Option<Message> {
|
||||
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 event.key_code {
|
||||
let direction = match key_code {
|
||||
KeyCode::Up => Some(Direction::Up),
|
||||
KeyCode::Down => Some(Direction::Down),
|
||||
KeyCode::Left => Some(Direction::Left),
|
||||
@ -126,7 +230,7 @@ fn handle_hotkey(event: pane_grid::KeyPressEvent) -> Option<Message> {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
match event.key_code {
|
||||
match key_code {
|
||||
KeyCode::V => Some(Message::SplitFocused(Axis::Vertical)),
|
||||
KeyCode::H => Some(Message::SplitFocused(Axis::Horizontal)),
|
||||
KeyCode::W => Some(Message::CloseFocused),
|
||||
@ -134,6 +238,13 @@ fn handle_hotkey(event: pane_grid::KeyPressEvent) -> Option<Message> {
|
||||
}
|
||||
}
|
||||
|
||||
struct Pane {
|
||||
pub is_pinned: bool,
|
||||
pub pin_button: button::State,
|
||||
pub content: Content,
|
||||
pub controls: Controls,
|
||||
}
|
||||
|
||||
struct Content {
|
||||
id: usize,
|
||||
scroll: scrollable::State,
|
||||
@ -142,6 +253,21 @@ struct Content {
|
||||
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 {
|
||||
@ -155,15 +281,15 @@ impl Content {
|
||||
fn view(
|
||||
&mut self,
|
||||
pane: pane_grid::Pane,
|
||||
focus: Option<pane_grid::Focus>,
|
||||
total_panes: usize,
|
||||
is_pinned: bool,
|
||||
) -> Element<Message> {
|
||||
let Content {
|
||||
id,
|
||||
scroll,
|
||||
split_horizontally,
|
||||
split_vertically,
|
||||
close,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let button = |state, label, message, style| {
|
||||
@ -196,7 +322,7 @@ impl Content {
|
||||
style::Button::Primary,
|
||||
));
|
||||
|
||||
if total_panes > 1 {
|
||||
if total_panes > 1 && !is_pinned {
|
||||
controls = controls.push(button(
|
||||
close,
|
||||
"Close",
|
||||
@ -209,7 +335,6 @@ impl Content {
|
||||
.width(Length::Fill)
|
||||
.spacing(10)
|
||||
.align_items(Align::Center)
|
||||
.push(Text::new(format!("Pane {}", id)).size(30))
|
||||
.push(controls);
|
||||
|
||||
Container::new(content)
|
||||
@ -217,14 +342,36 @@ impl Content {
|
||||
.height(Length::Fill)
|
||||
.padding(5)
|
||||
.center_y()
|
||||
.style(style::Pane {
|
||||
is_focused: focus.is_some(),
|
||||
})
|
||||
.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(
|
||||
@ -245,6 +392,25 @@ mod style {
|
||||
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,
|
||||
}
|
||||
@ -253,10 +419,11 @@ mod style {
|
||||
fn style(&self) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(SURFACE)),
|
||||
border_width: 2,
|
||||
border_color: Color {
|
||||
a: if self.is_focused { 1.0 } else { 0.3 },
|
||||
..Color::BLACK
|
||||
border_width: 2.0,
|
||||
border_color: if self.is_focused {
|
||||
Color::BLACK
|
||||
} else {
|
||||
Color::from_rgb(0.7, 0.7, 0.7)
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
@ -266,6 +433,8 @@ mod style {
|
||||
pub enum Button {
|
||||
Primary,
|
||||
Destructive,
|
||||
Control,
|
||||
Pin,
|
||||
}
|
||||
|
||||
impl button::StyleSheet for Button {
|
||||
@ -275,12 +444,14 @@ mod style {
|
||||
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,
|
||||
border_radius: 5.0,
|
||||
shadow_offset: Vector::new(0.0, 0.0),
|
||||
..button::Style::default()
|
||||
}
|
||||
@ -295,6 +466,8 @@ mod style {
|
||||
a: 0.2,
|
||||
..active.text_color
|
||||
}),
|
||||
Button::Control => Some(PANE_ID_COLOR_FOCUSED),
|
||||
Button::Pin => Some(HOVERED),
|
||||
};
|
||||
|
||||
button::Style {
|
||||
|
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",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["image", "debug", "tokio"] }
|
||||
iced = { path = "../..", features = ["image", "debug", "tokio_old"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[dependencies.serde]
|
||||
|
@ -1,9 +1,9 @@
|
||||
use iced::{
|
||||
button, futures, image, Align, Application, Button, Column, Command,
|
||||
Container, Element, Image, Length, Row, Settings, Text,
|
||||
button, futures, image, Align, Application, Button, Clipboard, Column,
|
||||
Command, Container, Element, Length, Row, Settings, Text,
|
||||
};
|
||||
|
||||
pub fn main() {
|
||||
pub fn main() -> iced::Result {
|
||||
Pokedex::run(Settings::default())
|
||||
}
|
||||
|
||||
@ -48,7 +48,11 @@ impl Application for Pokedex {
|
||||
format!("{} - Pokédex", subtitle)
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Message,
|
||||
_clipboard: &mut Clipboard,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::PokemonFound(Ok(pokemon)) => {
|
||||
*self = Pokedex::Loaded {
|
||||
@ -112,16 +116,20 @@ struct Pokemon {
|
||||
name: String,
|
||||
description: String,
|
||||
image: image::Handle,
|
||||
image_viewer: image::viewer::State,
|
||||
}
|
||||
|
||||
impl Pokemon {
|
||||
const TOTAL: u16 = 807;
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
Row::new()
|
||||
.spacing(20)
|
||||
.align_items(Align::Center)
|
||||
.push(Image::new(self.image.clone()))
|
||||
.push(image::Viewer::new(
|
||||
&mut self.image_viewer,
|
||||
self.image.clone(),
|
||||
))
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
@ -200,11 +208,15 @@ impl Pokemon {
|
||||
.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);
|
||||
let url = format!(
|
||||
"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{}.png",
|
||||
id
|
||||
);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
@ -251,7 +263,7 @@ mod style {
|
||||
background: Some(Background::Color(match self {
|
||||
Button::Primary => Color::from_rgb(0.11, 0.42, 0.87),
|
||||
})),
|
||||
border_radius: 12,
|
||||
border_radius: 12.0,
|
||||
shadow_offset: Vector::new(1.0, 1.0),
|
||||
text_color: Color::WHITE,
|
||||
..button::Style::default()
|
||||
|
@ -1,6 +1,6 @@
|
||||
use iced::{slider, Column, Element, ProgressBar, Sandbox, Settings, Slider};
|
||||
|
||||
pub fn main() {
|
||||
pub fn main() -> iced::Result {
|
||||
Progress::run(Settings::default())
|
||||
}
|
||||
|
||||
@ -36,12 +36,15 @@ impl Sandbox for Progress {
|
||||
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,
|
||||
))
|
||||
.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"] }
|
18
examples/qr_code/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
## QR Code Generator
|
||||
|
||||
A basic QR code generator that showcases the `QRCode` widget.
|
||||
|
||||
The __[`main`]__ file contains all the code of the example.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/heavyexhaustedaracari">
|
||||
<img src="https://thumbs.gfycat.com/HeavyExhaustedAracari-size_restricted.gif">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package qr_code
|
||||
```
|
||||
|
||||
[`main`]: src/main.rs
|
81
examples/qr_code/src/main.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use iced::qr_code::{self, QRCode};
|
||||
use iced::text_input::{self, TextInput};
|
||||
use iced::{
|
||||
Align, Column, Container, Element, Length, Sandbox, Settings, Text,
|
||||
};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
QRGenerator::run(Settings::default())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct QRGenerator {
|
||||
data: String,
|
||||
input: text_input::State,
|
||||
qr_code: Option<qr_code::State>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
DataChanged(String),
|
||||
}
|
||||
|
||||
impl Sandbox for QRGenerator {
|
||||
type Message = Message;
|
||||
|
||||
fn new() -> Self {
|
||||
QRGenerator {
|
||||
qr_code: qr_code::State::new("").ok(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("QR Code Generator - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::DataChanged(mut data) => {
|
||||
data.truncate(100);
|
||||
|
||||
self.qr_code = qr_code::State::new(&data).ok();
|
||||
self.data = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let title = Text::new("QR Code Generator")
|
||||
.size(70)
|
||||
.color([0.5, 0.5, 0.5]);
|
||||
|
||||
let input = TextInput::new(
|
||||
&mut self.input,
|
||||
"Type the data of your QR code here...",
|
||||
&self.data,
|
||||
Message::DataChanged,
|
||||
)
|
||||
.size(30)
|
||||
.padding(15);
|
||||
|
||||
let mut content = Column::new()
|
||||
.width(Length::Units(700))
|
||||
.spacing(20)
|
||||
.align_items(Align::Center)
|
||||
.push(title)
|
||||
.push(input);
|
||||
|
||||
if let Some(qr_code) = self.qr_code.as_mut() {
|
||||
content = content.push(QRCode::new(qr_code).cell_size(10));
|
||||
}
|
||||
|
||||
Container::new(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(20)
|
||||
.center_x()
|
||||
.center_y()
|
||||
.into()
|
||||
}
|
||||
}
|
9
examples/scrollable/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "scrollable"
|
||||
version = "0.1.0"
|
||||
authors = ["Clark Moody <clark@clarkmoody.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["debug"] }
|
15
examples/scrollable/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# Scrollable
|
||||
An example showcasing the various size and style options for the Scrollable.
|
||||
|
||||
All the example code is located in the __[`main`](src/main.rs)__ file.
|
||||
|
||||
<div align="center">
|
||||
<a href="./screenshot.png">
|
||||
<img src="./screenshot.png" height="640px">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package scrollable
|
||||
```
|
BIN
examples/scrollable/screenshot.png
Normal file
After Width: | Height: | Size: 145 KiB |
255
examples/scrollable/src/main.rs
Normal file
@ -0,0 +1,255 @@
|
||||
mod style;
|
||||
|
||||
use iced::{
|
||||
button, scrollable, Button, Column, Container, Element, Length,
|
||||
ProgressBar, Radio, Row, Rule, Sandbox, Scrollable, Settings, Space, Text,
|
||||
};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
ScrollableDemo::run(Settings::default())
|
||||
}
|
||||
|
||||
struct ScrollableDemo {
|
||||
theme: style::Theme,
|
||||
variants: Vec<Variant>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
ThemeChanged(style::Theme),
|
||||
ScrollToTop(usize),
|
||||
ScrollToBottom(usize),
|
||||
Scrolled(usize, f32),
|
||||
}
|
||||
|
||||
impl Sandbox for ScrollableDemo {
|
||||
type Message = Message;
|
||||
|
||||
fn new() -> Self {
|
||||
ScrollableDemo {
|
||||
theme: Default::default(),
|
||||
variants: Variant::all(),
|
||||
}
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Scrollable - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::ThemeChanged(theme) => self.theme = theme,
|
||||
Message::ScrollToTop(i) => {
|
||||
if let Some(variant) = self.variants.get_mut(i) {
|
||||
variant.scrollable.snap_to(0.0);
|
||||
|
||||
variant.latest_offset = 0.0;
|
||||
}
|
||||
}
|
||||
Message::ScrollToBottom(i) => {
|
||||
if let Some(variant) = self.variants.get_mut(i) {
|
||||
variant.scrollable.snap_to(1.0);
|
||||
|
||||
variant.latest_offset = 1.0;
|
||||
}
|
||||
}
|
||||
Message::Scrolled(i, offset) => {
|
||||
if let Some(variant) = self.variants.get_mut(i) {
|
||||
variant.latest_offset = offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let ScrollableDemo {
|
||||
theme, variants, ..
|
||||
} = self;
|
||||
|
||||
let choose_theme = style::Theme::ALL.iter().fold(
|
||||
Column::new().spacing(10).push(Text::new("Choose a theme:")),
|
||||
|column, option| {
|
||||
column.push(
|
||||
Radio::new(
|
||||
*option,
|
||||
&format!("{:?}", option),
|
||||
Some(*theme),
|
||||
Message::ThemeChanged,
|
||||
)
|
||||
.style(*theme),
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let scrollable_row = Row::with_children(
|
||||
variants
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.map(|(i, variant)| {
|
||||
let mut scrollable =
|
||||
Scrollable::new(&mut variant.scrollable)
|
||||
.padding(10)
|
||||
.spacing(10)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.on_scroll(move |offset| {
|
||||
Message::Scrolled(i, offset)
|
||||
})
|
||||
.style(*theme)
|
||||
.push(Text::new(variant.title))
|
||||
.push(
|
||||
Button::new(
|
||||
&mut variant.scroll_to_bottom,
|
||||
Text::new("Scroll to bottom"),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding(10)
|
||||
.on_press(Message::ScrollToBottom(i)),
|
||||
);
|
||||
|
||||
if let Some(scrollbar_width) = variant.scrollbar_width {
|
||||
scrollable = scrollable
|
||||
.scrollbar_width(scrollbar_width)
|
||||
.push(Text::new(format!(
|
||||
"scrollbar_width: {:?}",
|
||||
scrollbar_width
|
||||
)));
|
||||
}
|
||||
|
||||
if let Some(scrollbar_margin) = variant.scrollbar_margin {
|
||||
scrollable = scrollable
|
||||
.scrollbar_margin(scrollbar_margin)
|
||||
.push(Text::new(format!(
|
||||
"scrollbar_margin: {:?}",
|
||||
scrollbar_margin
|
||||
)));
|
||||
}
|
||||
|
||||
if let Some(scroller_width) = variant.scroller_width {
|
||||
scrollable = scrollable
|
||||
.scroller_width(scroller_width)
|
||||
.push(Text::new(format!(
|
||||
"scroller_width: {:?}",
|
||||
scroller_width
|
||||
)));
|
||||
}
|
||||
|
||||
scrollable = scrollable
|
||||
.push(Space::with_height(Length::Units(100)))
|
||||
.push(Text::new(
|
||||
"Some content that should wrap within the \
|
||||
scrollable. Let's output a lot of short words, so \
|
||||
that we'll make sure to see how wrapping works \
|
||||
with these scrollbars.",
|
||||
))
|
||||
.push(Space::with_height(Length::Units(1200)))
|
||||
.push(Text::new("Middle"))
|
||||
.push(Space::with_height(Length::Units(1200)))
|
||||
.push(Text::new("The End."))
|
||||
.push(
|
||||
Button::new(
|
||||
&mut variant.scroll_to_top,
|
||||
Text::new("Scroll to top"),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding(10)
|
||||
.on_press(Message::ScrollToTop(i)),
|
||||
);
|
||||
|
||||
Column::new()
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.spacing(10)
|
||||
.push(
|
||||
Container::new(scrollable)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(*theme),
|
||||
)
|
||||
.push(ProgressBar::new(
|
||||
0.0..=1.0,
|
||||
variant.latest_offset,
|
||||
))
|
||||
.into()
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.spacing(20)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
|
||||
let content = Column::new()
|
||||
.spacing(20)
|
||||
.padding(20)
|
||||
.push(choose_theme)
|
||||
.push(Rule::horizontal(20).style(self.theme))
|
||||
.push(scrollable_row);
|
||||
|
||||
Container::new(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_x()
|
||||
.center_y()
|
||||
.style(self.theme)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A version of a scrollable
|
||||
struct Variant {
|
||||
title: &'static str,
|
||||
scrollable: scrollable::State,
|
||||
scroll_to_top: button::State,
|
||||
scroll_to_bottom: button::State,
|
||||
scrollbar_width: Option<u16>,
|
||||
scrollbar_margin: Option<u16>,
|
||||
scroller_width: Option<u16>,
|
||||
latest_offset: f32,
|
||||
}
|
||||
|
||||
impl Variant {
|
||||
pub fn all() -> Vec<Self> {
|
||||
vec![
|
||||
Self {
|
||||
title: "Default Scrollbar",
|
||||
scrollable: scrollable::State::new(),
|
||||
scroll_to_top: button::State::new(),
|
||||
scroll_to_bottom: button::State::new(),
|
||||
scrollbar_width: None,
|
||||
scrollbar_margin: None,
|
||||
scroller_width: None,
|
||||
latest_offset: 0.0,
|
||||
},
|
||||
Self {
|
||||
title: "Slimmed & Margin",
|
||||
scrollable: scrollable::State::new(),
|
||||
scroll_to_top: button::State::new(),
|
||||
scroll_to_bottom: button::State::new(),
|
||||
scrollbar_width: Some(4),
|
||||
scrollbar_margin: Some(3),
|
||||
scroller_width: Some(4),
|
||||
latest_offset: 0.0,
|
||||
},
|
||||
Self {
|
||||
title: "Wide Scroller",
|
||||
scrollable: scrollable::State::new(),
|
||||
scroll_to_top: button::State::new(),
|
||||
scroll_to_bottom: button::State::new(),
|
||||
scrollbar_width: Some(4),
|
||||
scrollbar_margin: None,
|
||||
scroller_width: Some(10),
|
||||
latest_offset: 0.0,
|
||||
},
|
||||
Self {
|
||||
title: "Narrow Scroller",
|
||||
scrollable: scrollable::State::new(),
|
||||
scroll_to_top: button::State::new(),
|
||||
scroll_to_bottom: button::State::new(),
|
||||
scrollbar_width: Some(10),
|
||||
scrollbar_margin: None,
|
||||
scroller_width: Some(4),
|
||||
latest_offset: 0.0,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
190
examples/scrollable/src/style.rs
Normal file
@ -0,0 +1,190 @@
|
||||
use iced::{container, radio, rule, scrollable};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Theme {
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub const ALL: [Theme; 2] = [Theme::Light, Theme::Dark];
|
||||
}
|
||||
|
||||
impl Default for Theme {
|
||||
fn default() -> Theme {
|
||||
Theme::Light
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Theme> for Box<dyn container::StyleSheet> {
|
||||
fn from(theme: Theme) -> Self {
|
||||
match theme {
|
||||
Theme::Light => Default::default(),
|
||||
Theme::Dark => dark::Container.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Theme> for Box<dyn radio::StyleSheet> {
|
||||
fn from(theme: Theme) -> Self {
|
||||
match theme {
|
||||
Theme::Light => Default::default(),
|
||||
Theme::Dark => dark::Radio.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Theme> for Box<dyn scrollable::StyleSheet> {
|
||||
fn from(theme: Theme) -> Self {
|
||||
match theme {
|
||||
Theme::Light => Default::default(),
|
||||
Theme::Dark => dark::Scrollable.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Theme> for Box<dyn rule::StyleSheet> {
|
||||
fn from(theme: Theme) -> Self {
|
||||
match theme {
|
||||
Theme::Light => Default::default(),
|
||||
Theme::Dark => dark::Rule.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod dark {
|
||||
use iced::{container, radio, rule, scrollable, Color};
|
||||
|
||||
const BACKGROUND: Color = Color::from_rgb(
|
||||
0x36 as f32 / 255.0,
|
||||
0x39 as f32 / 255.0,
|
||||
0x3F as f32 / 255.0,
|
||||
);
|
||||
|
||||
const SURFACE: Color = Color::from_rgb(
|
||||
0x40 as f32 / 255.0,
|
||||
0x44 as f32 / 255.0,
|
||||
0x4B as f32 / 255.0,
|
||||
);
|
||||
|
||||
const ACCENT: Color = Color::from_rgb(
|
||||
0x6F as f32 / 255.0,
|
||||
0xFF as f32 / 255.0,
|
||||
0xE9 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 SCROLLBAR: Color = Color::from_rgb(
|
||||
0x2E as f32 / 255.0,
|
||||
0x33 as f32 / 255.0,
|
||||
0x38 as f32 / 255.0,
|
||||
);
|
||||
|
||||
const SCROLLER: Color = Color::from_rgb(
|
||||
0x20 as f32 / 255.0,
|
||||
0x22 as f32 / 255.0,
|
||||
0x25 as f32 / 255.0,
|
||||
);
|
||||
|
||||
pub struct Container;
|
||||
|
||||
impl container::StyleSheet for Container {
|
||||
fn style(&self) -> container::Style {
|
||||
container::Style {
|
||||
background: Color {
|
||||
a: 0.99,
|
||||
..BACKGROUND
|
||||
}
|
||||
.into(),
|
||||
text_color: Color::WHITE.into(),
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Radio;
|
||||
|
||||
impl radio::StyleSheet for Radio {
|
||||
fn active(&self) -> radio::Style {
|
||||
radio::Style {
|
||||
background: SURFACE.into(),
|
||||
dot_color: ACTIVE,
|
||||
border_width: 1.0,
|
||||
border_color: ACTIVE,
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self) -> radio::Style {
|
||||
radio::Style {
|
||||
background: Color { a: 0.5, ..SURFACE }.into(),
|
||||
..self.active()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Scrollable;
|
||||
|
||||
impl scrollable::StyleSheet for Scrollable {
|
||||
fn active(&self) -> scrollable::Scrollbar {
|
||||
scrollable::Scrollbar {
|
||||
background: Color {
|
||||
a: 0.8,
|
||||
..SCROLLBAR
|
||||
}
|
||||
.into(),
|
||||
border_radius: 2.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
scroller: scrollable::Scroller {
|
||||
color: Color { a: 0.7, ..SCROLLER },
|
||||
border_radius: 2.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self) -> scrollable::Scrollbar {
|
||||
let active = self.active();
|
||||
|
||||
scrollable::Scrollbar {
|
||||
background: SCROLLBAR.into(),
|
||||
scroller: scrollable::Scroller {
|
||||
color: SCROLLER,
|
||||
..active.scroller
|
||||
},
|
||||
..active
|
||||
}
|
||||
}
|
||||
|
||||
fn dragging(&self) -> scrollable::Scrollbar {
|
||||
let hovered = self.hovered();
|
||||
|
||||
scrollable::Scrollbar {
|
||||
scroller: scrollable::Scroller {
|
||||
color: ACCENT,
|
||||
..hovered.scroller
|
||||
},
|
||||
..hovered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Rule;
|
||||
|
||||
impl rule::StyleSheet for Rule {
|
||||
fn style(&self) -> rule::Style {
|
||||
rule::Style {
|
||||
color: SURFACE,
|
||||
width: 2,
|
||||
radius: 1.0,
|
||||
fill_mode: rule::FillMode::Percent(30.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,11 +5,6 @@ authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
canvas = []
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["canvas", "async-std", "debug"] }
|
||||
iced_native = { path = "../../native" }
|
||||
async-std = { version = "1.0", features = ["unstable"] }
|
||||
rand = "0.7"
|
||||
iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
|
||||
rand = "0.8.3"
|
||||
|
@ -7,13 +7,14 @@
|
||||
//!
|
||||
//! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system
|
||||
use iced::{
|
||||
canvas, executor, window, Application, Canvas, Color, Command, Container,
|
||||
Element, Length, Point, Settings, Size, Subscription, Vector,
|
||||
canvas::{self, Cursor, Path, Stroke},
|
||||
executor, time, window, Application, Canvas, Clipboard, Color, Command,
|
||||
Element, Length, Point, Rectangle, Settings, Size, Subscription, Vector,
|
||||
};
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
pub fn main() {
|
||||
pub fn main() -> iced::Result {
|
||||
SolarSystem::run(Settings {
|
||||
antialiasing: true,
|
||||
..Settings::default()
|
||||
@ -22,7 +23,6 @@ pub fn main() {
|
||||
|
||||
struct SolarSystem {
|
||||
state: State,
|
||||
solar_system: canvas::layer::Cache<State>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@ -39,7 +39,6 @@ impl Application for SolarSystem {
|
||||
(
|
||||
SolarSystem {
|
||||
state: State::new(),
|
||||
solar_system: canvas::layer::Cache::new(),
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
@ -49,11 +48,14 @@ impl Application for SolarSystem {
|
||||
String::from("Solar system - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Message,
|
||||
_clipboard: &mut Clipboard,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::Tick(instant) => {
|
||||
self.state.update(instant);
|
||||
self.solar_system.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,24 +68,20 @@ impl Application for SolarSystem {
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let canvas = Canvas::new()
|
||||
Canvas::new(&mut self.state)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.push(self.solar_system.with(&self.state));
|
||||
|
||||
Container::new(canvas)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_x()
|
||||
.center_y()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct State {
|
||||
space_cache: canvas::Cache,
|
||||
system_cache: canvas::Cache,
|
||||
cursor_position: Point,
|
||||
start: Instant,
|
||||
current: Instant,
|
||||
now: Instant,
|
||||
stars: Vec<(Point, f32)>,
|
||||
}
|
||||
|
||||
@ -99,150 +97,120 @@ impl State {
|
||||
let (width, height) = window::Settings::default().size;
|
||||
|
||||
State {
|
||||
space_cache: Default::default(),
|
||||
system_cache: Default::default(),
|
||||
cursor_position: Point::ORIGIN,
|
||||
start: now,
|
||||
current: now,
|
||||
stars: {
|
||||
use rand::Rng;
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
(0..100)
|
||||
.map(|_| {
|
||||
(
|
||||
Point::new(
|
||||
rng.gen_range(0.0, width as f32),
|
||||
rng.gen_range(0.0, height as f32),
|
||||
),
|
||||
rng.gen_range(0.5, 1.0),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
now,
|
||||
stars: Self::generate_stars(width, height),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, now: Instant) {
|
||||
self.current = now;
|
||||
self.now = now;
|
||||
self.system_cache.clear();
|
||||
}
|
||||
|
||||
fn generate_stars(width: u32, height: u32) -> Vec<(Point, f32)> {
|
||||
use rand::Rng;
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
(0..100)
|
||||
.map(|_| {
|
||||
(
|
||||
Point::new(
|
||||
rng.gen_range(
|
||||
(-(width as f32) / 2.0)..(width as f32 / 2.0),
|
||||
),
|
||||
rng.gen_range(
|
||||
(-(height as f32) / 2.0)..(height as f32 / 2.0),
|
||||
),
|
||||
),
|
||||
rng.gen_range(0.5..1.0),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl canvas::Drawable for State {
|
||||
fn draw(&self, frame: &mut canvas::Frame) {
|
||||
use canvas::{Fill, Path, Stroke};
|
||||
impl<Message> canvas::Program<Message> for State {
|
||||
fn draw(
|
||||
&self,
|
||||
bounds: Rectangle,
|
||||
_cursor: Cursor,
|
||||
) -> Vec<canvas::Geometry> {
|
||||
use std::f32::consts::PI;
|
||||
|
||||
let center = frame.center();
|
||||
let background = self.space_cache.draw(bounds.size(), |frame| {
|
||||
let space = Path::rectangle(Point::new(0.0, 0.0), frame.size());
|
||||
|
||||
let space = Path::new(|path| {
|
||||
path.rectangle(Point::new(0.0, 0.0), frame.size())
|
||||
});
|
||||
|
||||
let stars = Path::new(|path| {
|
||||
for (p, size) in &self.stars {
|
||||
path.rectangle(*p, Size::new(*size, *size));
|
||||
}
|
||||
});
|
||||
|
||||
let sun = Path::new(|path| path.circle(center, Self::SUN_RADIUS));
|
||||
let orbit = Path::new(|path| path.circle(center, Self::ORBIT_RADIUS));
|
||||
|
||||
frame.fill(&space, Fill::Color(Color::BLACK));
|
||||
frame.fill(&stars, Fill::Color(Color::WHITE));
|
||||
frame.fill(&sun, Fill::Color(Color::from_rgb8(0xF9, 0xD7, 0x1C)));
|
||||
frame.stroke(
|
||||
&orbit,
|
||||
Stroke {
|
||||
width: 1.0,
|
||||
color: Color::from_rgba8(0, 153, 255, 0.1),
|
||||
..Stroke::default()
|
||||
},
|
||||
);
|
||||
|
||||
let elapsed = self.current - self.start;
|
||||
let elapsed_seconds = elapsed.as_secs() as f32;
|
||||
let elapsed_millis = elapsed.subsec_millis() as f32;
|
||||
|
||||
frame.with_save(|frame| {
|
||||
frame.translate(Vector::new(center.x, center.y));
|
||||
frame.rotate(
|
||||
(2.0 * PI / 60.0) * elapsed_seconds
|
||||
+ (2.0 * PI / 60_000.0) * elapsed_millis,
|
||||
);
|
||||
frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0));
|
||||
|
||||
let earth = Path::new(|path| {
|
||||
path.circle(Point::ORIGIN, Self::EARTH_RADIUS)
|
||||
let stars = Path::new(|path| {
|
||||
for (p, size) in &self.stars {
|
||||
path.rectangle(*p, Size::new(*size, *size));
|
||||
}
|
||||
});
|
||||
|
||||
let shadow = Path::new(|path| {
|
||||
path.rectangle(
|
||||
frame.fill(&space, Color::BLACK);
|
||||
|
||||
frame.translate(frame.center() - Point::ORIGIN);
|
||||
frame.fill(&stars, Color::WHITE);
|
||||
});
|
||||
|
||||
let system = self.system_cache.draw(bounds.size(), |frame| {
|
||||
let center = frame.center();
|
||||
|
||||
let sun = Path::circle(center, Self::SUN_RADIUS);
|
||||
let orbit = Path::circle(center, Self::ORBIT_RADIUS);
|
||||
|
||||
frame.fill(&sun, Color::from_rgb8(0xF9, 0xD7, 0x1C));
|
||||
frame.stroke(
|
||||
&orbit,
|
||||
Stroke {
|
||||
width: 1.0,
|
||||
color: Color::from_rgba8(0, 153, 255, 0.1),
|
||||
..Stroke::default()
|
||||
},
|
||||
);
|
||||
|
||||
let elapsed = self.now - self.start;
|
||||
let rotation = (2.0 * PI / 60.0) * elapsed.as_secs() as f32
|
||||
+ (2.0 * PI / 60_000.0) * elapsed.subsec_millis() as f32;
|
||||
|
||||
frame.with_save(|frame| {
|
||||
frame.translate(Vector::new(center.x, center.y));
|
||||
frame.rotate(rotation);
|
||||
frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0));
|
||||
|
||||
let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS);
|
||||
let shadow = Path::rectangle(
|
||||
Point::new(0.0, -Self::EARTH_RADIUS),
|
||||
Size::new(
|
||||
Self::EARTH_RADIUS * 4.0,
|
||||
Self::EARTH_RADIUS * 2.0,
|
||||
),
|
||||
)
|
||||
});
|
||||
|
||||
frame.fill(&earth, Fill::Color(Color::from_rgb8(0x6B, 0x93, 0xD6)));
|
||||
|
||||
frame.with_save(|frame| {
|
||||
frame.rotate(
|
||||
((2.0 * PI) / 6.0) * elapsed_seconds
|
||||
+ ((2.0 * PI) / 6_000.0) * elapsed_millis,
|
||||
);
|
||||
frame.translate(Vector::new(0.0, Self::MOON_DISTANCE));
|
||||
|
||||
let moon = Path::new(|path| {
|
||||
path.circle(Point::ORIGIN, Self::MOON_RADIUS)
|
||||
frame.fill(&earth, Color::from_rgb8(0x6B, 0x93, 0xD6));
|
||||
|
||||
frame.with_save(|frame| {
|
||||
frame.rotate(rotation * 10.0);
|
||||
frame.translate(Vector::new(0.0, Self::MOON_DISTANCE));
|
||||
|
||||
let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS);
|
||||
frame.fill(&moon, Color::WHITE);
|
||||
});
|
||||
|
||||
frame.fill(&moon, Fill::Color(Color::WHITE));
|
||||
frame.fill(
|
||||
&shadow,
|
||||
Color {
|
||||
a: 0.7,
|
||||
..Color::BLACK
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
frame.fill(
|
||||
&shadow,
|
||||
Fill::Color(Color {
|
||||
a: 0.7,
|
||||
..Color::BLACK
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mod time {
|
||||
use iced::futures;
|
||||
use std::time::Instant;
|
||||
|
||||
pub fn every(duration: std::time::Duration) -> iced::Subscription<Instant> {
|
||||
iced::Subscription::from_recipe(Every(duration))
|
||||
}
|
||||
|
||||
struct Every(std::time::Duration);
|
||||
|
||||
impl<H, I> iced_native::subscription::Recipe<H, I> for Every
|
||||
where
|
||||
H: std::hash::Hasher,
|
||||
{
|
||||
type Output = Instant;
|
||||
|
||||
fn hash(&self, state: &mut H) {
|
||||
use std::hash::Hash;
|
||||
|
||||
std::any::TypeId::of::<Self>().hash(state);
|
||||
self.0.hash(state);
|
||||
}
|
||||
|
||||
fn stream(
|
||||
self: Box<Self>,
|
||||
_input: futures::stream::BoxStream<'static, I>,
|
||||
) -> futures::stream::BoxStream<'static, Self::Output> {
|
||||
use futures::stream::StreamExt;
|
||||
|
||||
async_std::stream::interval(self.0)
|
||||
.map(|_| Instant::now())
|
||||
.boxed()
|
||||
}
|
||||
|
||||
vec![background, system]
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,4 @@ edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../.." }
|
||||
iced_native = { path = "../../native" }
|
||||
iced_futures = { path = "../../futures", features = ["async-std"] }
|
||||
async-std = { version = "1.0", features = ["unstable"] }
|
||||
iced = { path = "../..", features = ["smol"] }
|
||||
|
@ -1,10 +1,11 @@
|
||||
use iced::{
|
||||
button, Align, Application, Button, Column, Command, Container, Element,
|
||||
HorizontalAlignment, Length, Row, Settings, Subscription, Text,
|
||||
button, executor, time, Align, Application, Button, Clipboard, Column,
|
||||
Command, Container, Element, HorizontalAlignment, Length, Row, Settings,
|
||||
Subscription, Text,
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub fn main() {
|
||||
pub fn main() -> iced::Result {
|
||||
Stopwatch::run(Settings::default())
|
||||
}
|
||||
|
||||
@ -28,7 +29,7 @@ enum Message {
|
||||
}
|
||||
|
||||
impl Application for Stopwatch {
|
||||
type Executor = iced_futures::executor::AsyncStd;
|
||||
type Executor = executor::Default;
|
||||
type Message = Message;
|
||||
type Flags = ();
|
||||
|
||||
@ -48,7 +49,11 @@ impl Application for Stopwatch {
|
||||
String::from("Stopwatch - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Message,
|
||||
_clipboard: &mut Clipboard,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::Toggle => match self.state {
|
||||
State::Idle => {
|
||||
@ -143,43 +148,6 @@ impl Application for Stopwatch {
|
||||
}
|
||||
}
|
||||
|
||||
mod time {
|
||||
use iced::futures;
|
||||
|
||||
pub fn every(
|
||||
duration: std::time::Duration,
|
||||
) -> iced::Subscription<std::time::Instant> {
|
||||
iced::Subscription::from_recipe(Every(duration))
|
||||
}
|
||||
|
||||
struct Every(std::time::Duration);
|
||||
|
||||
impl<H, I> iced_native::subscription::Recipe<H, I> for Every
|
||||
where
|
||||
H: std::hash::Hasher,
|
||||
{
|
||||
type Output = std::time::Instant;
|
||||
|
||||
fn hash(&self, state: &mut H) {
|
||||
use std::hash::Hash;
|
||||
|
||||
std::any::TypeId::of::<Self>().hash(state);
|
||||
self.0.hash(state);
|
||||
}
|
||||
|
||||
fn stream(
|
||||
self: Box<Self>,
|
||||
_input: futures::stream::BoxStream<'static, I>,
|
||||
) -> futures::stream::BoxStream<'static, Self::Output> {
|
||||
use futures::stream::StreamExt;
|
||||
|
||||
async_std::stream::interval(self.0)
|
||||
.map(|_| std::time::Instant::now())
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod style {
|
||||
use iced::{button, Background, Color, Vector};
|
||||
|
||||
@ -197,7 +165,7 @@ mod style {
|
||||
Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5),
|
||||
Button::Destructive => Color::from_rgb(0.8, 0.2, 0.2),
|
||||
})),
|
||||
border_radius: 12,
|
||||
border_radius: 12.0,
|
||||
shadow_offset: Vector::new(1.0, 1.0),
|
||||
text_color: Color::WHITE,
|
||||
..button::Style::default()
|
||||
|
@ -1,10 +1,10 @@
|
||||
use iced::{
|
||||
button, scrollable, slider, text_input, Align, Button, Checkbox, Column,
|
||||
Container, Element, Length, ProgressBar, Radio, Row, Sandbox, Scrollable,
|
||||
Settings, Slider, Space, Text, TextInput,
|
||||
Container, Element, Length, ProgressBar, Radio, Row, Rule, Sandbox,
|
||||
Scrollable, Settings, Slider, Space, Text, TextInput, Toggler,
|
||||
};
|
||||
|
||||
pub fn main() {
|
||||
pub fn main() -> iced::Result {
|
||||
Styling::run(Settings::default())
|
||||
}
|
||||
|
||||
@ -17,7 +17,8 @@ struct Styling {
|
||||
button: button::State,
|
||||
slider: slider::State,
|
||||
slider_value: f32,
|
||||
toggle_value: bool,
|
||||
checkbox_value: bool,
|
||||
toggler_value: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -27,6 +28,7 @@ enum Message {
|
||||
ButtonPressed,
|
||||
SliderChanged(f32),
|
||||
CheckboxToggled(bool),
|
||||
TogglerToggled(bool),
|
||||
}
|
||||
|
||||
impl Sandbox for Styling {
|
||||
@ -44,9 +46,10 @@ impl Sandbox for Styling {
|
||||
match message {
|
||||
Message::ThemeChanged(theme) => self.theme = theme,
|
||||
Message::InputChanged(value) => self.input_value = value,
|
||||
Message::ButtonPressed => (),
|
||||
Message::ButtonPressed => {}
|
||||
Message::SliderChanged(value) => self.slider_value = value,
|
||||
Message::CheckboxToggled(value) => self.toggle_value = value,
|
||||
Message::CheckboxToggled(value) => self.checkbox_value = value,
|
||||
Message::TogglerToggled(value) => self.toggler_value = value,
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,11 +104,19 @@ impl Sandbox for Styling {
|
||||
.push(Text::new("You did it!"));
|
||||
|
||||
let checkbox = Checkbox::new(
|
||||
self.toggle_value,
|
||||
"Toggle me!",
|
||||
self.checkbox_value,
|
||||
"Check me!",
|
||||
Message::CheckboxToggled,
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.style(self.theme);
|
||||
|
||||
let toggler = Toggler::new(
|
||||
self.toggler_value,
|
||||
String::from("Toggle me!"),
|
||||
Message::TogglerToggled,
|
||||
)
|
||||
.width(Length::Shrink)
|
||||
.spacing(10)
|
||||
.style(self.theme);
|
||||
|
||||
let content = Column::new()
|
||||
@ -113,15 +124,24 @@ impl Sandbox for Styling {
|
||||
.padding(20)
|
||||
.max_width(600)
|
||||
.push(choose_theme)
|
||||
.push(Rule::horizontal(38).style(self.theme))
|
||||
.push(Row::new().spacing(10).push(text_input).push(button))
|
||||
.push(slider)
|
||||
.push(progress_bar)
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.height(Length::Units(100))
|
||||
.align_items(Align::Center)
|
||||
.push(scrollable)
|
||||
.push(checkbox),
|
||||
.push(Rule::vertical(38).style(self.theme))
|
||||
.push(
|
||||
Column::new()
|
||||
.width(Length::Shrink)
|
||||
.spacing(20)
|
||||
.push(checkbox)
|
||||
.push(toggler),
|
||||
),
|
||||
);
|
||||
|
||||
Container::new(content)
|
||||
@ -136,8 +156,8 @@ impl Sandbox for Styling {
|
||||
|
||||
mod style {
|
||||
use iced::{
|
||||
button, checkbox, container, progress_bar, radio, scrollable, slider,
|
||||
text_input,
|
||||
button, checkbox, container, progress_bar, radio, rule, scrollable,
|
||||
slider, text_input, toggler,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@ -228,18 +248,34 @@ mod style {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Theme> for Box<dyn toggler::StyleSheet> {
|
||||
fn from(theme: Theme) -> Self {
|
||||
match theme {
|
||||
Theme::Light => Default::default(),
|
||||
Theme::Dark => dark::Toggler.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Theme> for Box<dyn rule::StyleSheet> {
|
||||
fn from(theme: Theme) -> Self {
|
||||
match theme {
|
||||
Theme::Light => Default::default(),
|
||||
Theme::Dark => dark::Rule.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod light {
|
||||
use iced::{button, Background, Color, Vector};
|
||||
use iced::{button, Color, Vector};
|
||||
|
||||
pub struct Button;
|
||||
|
||||
impl button::StyleSheet for Button {
|
||||
fn active(&self) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(
|
||||
0.11, 0.42, 0.87,
|
||||
))),
|
||||
border_radius: 12,
|
||||
background: Color::from_rgb(0.11, 0.42, 0.87).into(),
|
||||
border_radius: 12.0,
|
||||
shadow_offset: Vector::new(1.0, 1.0),
|
||||
text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE),
|
||||
..button::Style::default()
|
||||
@ -258,8 +294,8 @@ mod style {
|
||||
|
||||
mod dark {
|
||||
use iced::{
|
||||
button, checkbox, container, progress_bar, radio, scrollable,
|
||||
slider, text_input, Background, Color,
|
||||
button, checkbox, container, progress_bar, radio, rule, scrollable,
|
||||
slider, text_input, toggler, Color,
|
||||
};
|
||||
|
||||
const SURFACE: Color = Color::from_rgb(
|
||||
@ -291,10 +327,8 @@ mod style {
|
||||
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),
|
||||
background: Color::from_rgb8(0x36, 0x39, 0x3F).into(),
|
||||
text_color: Color::WHITE.into(),
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
@ -305,16 +339,16 @@ mod style {
|
||||
impl radio::StyleSheet for Radio {
|
||||
fn active(&self) -> radio::Style {
|
||||
radio::Style {
|
||||
background: Background::Color(SURFACE),
|
||||
background: SURFACE.into(),
|
||||
dot_color: ACTIVE,
|
||||
border_width: 1,
|
||||
border_width: 1.0,
|
||||
border_color: ACTIVE,
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self) -> radio::Style {
|
||||
radio::Style {
|
||||
background: Background::Color(Color { a: 0.5, ..SURFACE }),
|
||||
background: Color { a: 0.5, ..SURFACE }.into(),
|
||||
..self.active()
|
||||
}
|
||||
}
|
||||
@ -325,16 +359,16 @@ mod style {
|
||||
impl text_input::StyleSheet for TextInput {
|
||||
fn active(&self) -> text_input::Style {
|
||||
text_input::Style {
|
||||
background: Background::Color(SURFACE),
|
||||
border_radius: 2,
|
||||
border_width: 0,
|
||||
background: SURFACE.into(),
|
||||
border_radius: 2.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
}
|
||||
}
|
||||
|
||||
fn focused(&self) -> text_input::Style {
|
||||
text_input::Style {
|
||||
border_width: 1,
|
||||
border_width: 1.0,
|
||||
border_color: ACCENT,
|
||||
..self.active()
|
||||
}
|
||||
@ -342,7 +376,7 @@ mod style {
|
||||
|
||||
fn hovered(&self) -> text_input::Style {
|
||||
text_input::Style {
|
||||
border_width: 1,
|
||||
border_width: 1.0,
|
||||
border_color: Color { a: 0.3, ..ACCENT },
|
||||
..self.focused()
|
||||
}
|
||||
@ -366,8 +400,8 @@ mod style {
|
||||
impl button::StyleSheet for Button {
|
||||
fn active(&self) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(ACTIVE)),
|
||||
border_radius: 3,
|
||||
background: ACTIVE.into(),
|
||||
border_radius: 3.0,
|
||||
text_color: Color::WHITE,
|
||||
..button::Style::default()
|
||||
}
|
||||
@ -375,7 +409,7 @@ mod style {
|
||||
|
||||
fn hovered(&self) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(HOVERED)),
|
||||
background: HOVERED.into(),
|
||||
text_color: Color::WHITE,
|
||||
..self.active()
|
||||
}
|
||||
@ -383,7 +417,7 @@ mod style {
|
||||
|
||||
fn pressed(&self) -> button::Style {
|
||||
button::Style {
|
||||
border_width: 1,
|
||||
border_width: 1.0,
|
||||
border_color: Color::WHITE,
|
||||
..self.hovered()
|
||||
}
|
||||
@ -395,14 +429,14 @@ mod style {
|
||||
impl scrollable::StyleSheet for Scrollable {
|
||||
fn active(&self) -> scrollable::Scrollbar {
|
||||
scrollable::Scrollbar {
|
||||
background: Some(Background::Color(SURFACE)),
|
||||
border_radius: 2,
|
||||
border_width: 0,
|
||||
background: SURFACE.into(),
|
||||
border_radius: 2.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
scroller: scrollable::Scroller {
|
||||
color: ACTIVE,
|
||||
border_radius: 2,
|
||||
border_width: 0,
|
||||
border_radius: 2.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
}
|
||||
@ -412,10 +446,7 @@ mod style {
|
||||
let active = self.active();
|
||||
|
||||
scrollable::Scrollbar {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.5,
|
||||
..SURFACE
|
||||
})),
|
||||
background: Color { a: 0.5, ..SURFACE }.into(),
|
||||
scroller: scrollable::Scroller {
|
||||
color: HOVERED,
|
||||
..active.scroller
|
||||
@ -444,9 +475,9 @@ mod style {
|
||||
slider::Style {
|
||||
rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }),
|
||||
handle: slider::Handle {
|
||||
shape: slider::HandleShape::Circle { radius: 9 },
|
||||
shape: slider::HandleShape::Circle { radius: 9.0 },
|
||||
color: ACTIVE,
|
||||
border_width: 0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
}
|
||||
@ -482,9 +513,9 @@ mod style {
|
||||
impl progress_bar::StyleSheet for ProgressBar {
|
||||
fn style(&self) -> progress_bar::Style {
|
||||
progress_bar::Style {
|
||||
background: Background::Color(SURFACE),
|
||||
bar: Background::Color(ACTIVE),
|
||||
border_radius: 10,
|
||||
background: SURFACE.into(),
|
||||
bar: ACTIVE.into(),
|
||||
border_radius: 10.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -494,27 +525,67 @@ mod style {
|
||||
impl checkbox::StyleSheet for Checkbox {
|
||||
fn active(&self, is_checked: bool) -> checkbox::Style {
|
||||
checkbox::Style {
|
||||
background: Background::Color(if is_checked {
|
||||
ACTIVE
|
||||
} else {
|
||||
SURFACE
|
||||
}),
|
||||
background: if is_checked { ACTIVE } else { SURFACE }
|
||||
.into(),
|
||||
checkmark_color: Color::WHITE,
|
||||
border_radius: 2,
|
||||
border_width: 1,
|
||||
border_radius: 2.0,
|
||||
border_width: 1.0,
|
||||
border_color: ACTIVE,
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, is_checked: bool) -> checkbox::Style {
|
||||
checkbox::Style {
|
||||
background: Background::Color(Color {
|
||||
background: Color {
|
||||
a: 0.8,
|
||||
..if is_checked { ACTIVE } else { SURFACE }
|
||||
}),
|
||||
}
|
||||
.into(),
|
||||
..self.active(is_checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Toggler;
|
||||
|
||||
impl toggler::StyleSheet for Toggler {
|
||||
fn active(&self, is_active: bool) -> toggler::Style {
|
||||
toggler::Style {
|
||||
background: if is_active { ACTIVE } else { SURFACE },
|
||||
background_border: None,
|
||||
foreground: if is_active { Color::WHITE } else { ACTIVE },
|
||||
foreground_border: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, is_active: bool) -> toggler::Style {
|
||||
toggler::Style {
|
||||
background: if is_active { ACTIVE } else { SURFACE },
|
||||
background_border: None,
|
||||
foreground: if is_active {
|
||||
Color {
|
||||
a: 0.5,
|
||||
..Color::WHITE
|
||||
}
|
||||
} else {
|
||||
Color { a: 0.5, ..ACTIVE }
|
||||
},
|
||||
foreground_border: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Rule;
|
||||
|
||||
impl rule::StyleSheet for Rule {
|
||||
fn style(&self) -> rule::Style {
|
||||
rule::Style {
|
||||
color: SURFACE,
|
||||
width: 2,
|
||||
radius: 1.0,
|
||||
fill_mode: rule::FillMode::Padded(15),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use iced::{Container, Element, Length, Sandbox, Settings, Svg};
|
||||
|
||||
pub fn main() {
|
||||
pub fn main() -> iced::Result {
|
||||
Tiger::run(Settings::default())
|
||||
}
|
||||
|
||||
|
@ -6,13 +6,13 @@ edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["async-std"] }
|
||||
iced = { path = "../..", features = ["async-std", "debug"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
async-std = "1.0"
|
||||
directories = "2.0"
|
||||
directories-next = "2.0"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
web-sys = { version = "0.3", features = ["Window", "Storage"] }
|
||||
|
@ -1,11 +1,11 @@
|
||||
use iced::{
|
||||
button, scrollable, text_input, Align, Application, Button, Checkbox,
|
||||
Column, Command, Container, Element, Font, HorizontalAlignment, Length,
|
||||
Row, Scrollable, Settings, Text, TextInput,
|
||||
Clipboard, Column, Command, Container, Element, Font, HorizontalAlignment,
|
||||
Length, Row, Scrollable, Settings, Text, TextInput,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn main() {
|
||||
pub fn main() -> iced::Result {
|
||||
Todos::run(Settings::default())
|
||||
}
|
||||
|
||||
@ -58,7 +58,11 @@ impl Application for Todos {
|
||||
format!("Todos{} - Iced", if dirty { "*" } else { "" })
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Message,
|
||||
_clipboard: &mut Clipboard,
|
||||
) -> Command<Message> {
|
||||
match self {
|
||||
Todos::Loading => {
|
||||
match message {
|
||||
@ -425,7 +429,7 @@ impl Filter {
|
||||
}
|
||||
}
|
||||
|
||||
fn loading_message() -> Element<'static, Message> {
|
||||
fn loading_message<'a>() -> Element<'a, Message> {
|
||||
Container::new(
|
||||
Text::new("Loading...")
|
||||
.horizontal_alignment(HorizontalAlignment::Center)
|
||||
@ -437,7 +441,7 @@ fn loading_message() -> Element<'static, Message> {
|
||||
.into()
|
||||
}
|
||||
|
||||
fn empty_message(message: &str) -> Element<'static, Message> {
|
||||
fn empty_message<'a>(message: &str) -> Element<'a, Message> {
|
||||
Container::new(
|
||||
Text::new(message)
|
||||
.width(Length::Fill)
|
||||
@ -489,7 +493,6 @@ enum LoadError {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum SaveError {
|
||||
DirectoryError,
|
||||
FileError,
|
||||
WriteError,
|
||||
FormatError,
|
||||
@ -499,7 +502,7 @@ enum SaveError {
|
||||
impl SavedState {
|
||||
fn path() -> std::path::PathBuf {
|
||||
let mut path = if let Some(project_dirs) =
|
||||
directories::ProjectDirs::from("rs", "Iced", "Todos")
|
||||
directories_next::ProjectDirs::from("rs", "Iced", "Todos")
|
||||
{
|
||||
project_dirs.data_dir().into()
|
||||
} else {
|
||||
@ -538,7 +541,7 @@ impl SavedState {
|
||||
if let Some(dir) = path.parent() {
|
||||
async_std::fs::create_dir_all(dir)
|
||||
.await
|
||||
.map_err(|_| SaveError::DirectoryError)?;
|
||||
.map_err(|_| SaveError::FileError)?;
|
||||
}
|
||||
|
||||
{
|
||||
@ -611,7 +614,7 @@ mod style {
|
||||
background: Some(Background::Color(
|
||||
Color::from_rgb(0.2, 0.2, 0.7),
|
||||
)),
|
||||
border_radius: 10,
|
||||
border_radius: 10.0,
|
||||
text_color: Color::WHITE,
|
||||
..button::Style::default()
|
||||
}
|
||||
@ -627,7 +630,7 @@ mod style {
|
||||
background: Some(Background::Color(Color::from_rgb(
|
||||
0.8, 0.2, 0.2,
|
||||
))),
|
||||
border_radius: 5,
|
||||
border_radius: 5.0,
|
||||
text_color: Color::WHITE,
|
||||
shadow_offset: Vector::new(1.0, 1.0),
|
||||
..button::Style::default()
|
||||
|
9
examples/tooltip/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "tooltip"
|
||||
version = "0.1.0"
|
||||
authors = ["Yusuf Bera Ertan <y.bera003.06@protonmail.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["debug"] }
|