Compare commits
433 Commits
theme-file
...
main
Author | SHA1 | Date |
---|---|---|
Sasha Koshka | 501eb34922 | |
Sasha Koshka | 54ea1c283f | |
Sasha Koshka | 33c787d70b | |
Sasha Koshka | 794ab1b5e8 | |
Sasha Koshka | 83d0b32fba | |
Sasha Koshka | ae12945676 | |
Sasha Koshka | f3185999a2 | |
Sasha Koshka | 8ad14cd542 | |
Sasha Koshka | 8d587ae3b4 | |
Sasha Koshka | 4f8469c359 | |
Sasha Koshka | 6e1369da5c | |
Sasha Koshka | abe63f4118 | |
Sasha Koshka | cd6d8f3ff6 | |
Sasha Koshka | 37df313544 | |
Sasha Koshka | 2ac23185e6 | |
Sasha Koshka | 6241be1969 | |
Sasha Koshka | 9588996bd8 | |
Sasha Koshka | 2e3af402d5 | |
Sasha Koshka | 69e73a7b84 | |
Sasha Koshka | b84e444697 | |
Sasha Koshka | b51eb79033 | |
Sasha Koshka | 8e5ad8f385 | |
Sasha Koshka | b479ba8f0f | |
Sasha Koshka | 567358bf4c | |
Sasha Koshka | 10d5358390 | |
Sasha Koshka | 09d360826b | |
Sasha Koshka | b3a9bba255 | |
Sasha Koshka | 6acd8be05b | |
Sasha Koshka | 39b8b96513 | |
Sasha Koshka | 363779a947 | |
Sasha Koshka | cd8371a3f3 | |
Sasha Koshka | 4c9743387b | |
Sasha Koshka | e3d194562c | |
Sasha Koshka | 7472cf52d9 | |
Sasha Koshka | 8d6deb3456 | |
Sasha Koshka | cc03440125 | |
Sasha Koshka | cfdebddbdd | |
Sasha Koshka | 8a8e63ba05 | |
Sasha Koshka | 6578480195 | |
Sasha Koshka | 72fc28e223 | |
Sasha Koshka | 9e754cdb59 | |
Sasha Koshka | fb25496f34 | |
Sasha Koshka | 9cfbe0aa01 | |
Sasha Koshka | e0be332953 | |
Sasha Koshka | acc4148a18 | |
Sasha Koshka | 3caa9c53ef | |
Sasha Koshka | 4a723ff296 | |
Sasha Koshka | de4bce8c46 | |
Sasha Koshka | ca27a810c7 | |
Sasha Koshka | 77e1151bd8 | |
Sasha Koshka | a2a9af3311 | |
Sasha Koshka | 800ee2570f | |
Sasha Koshka | b92bdced9c | |
Sasha Koshka | 85f995aa10 | |
Sasha Koshka | 7b7005c068 | |
Sasha Koshka | d5d2cc1f4d | |
Sasha Koshka | dfdd721303 | |
Sasha Koshka | fe3ac7ec1e | |
Sasha Koshka | f5180efc8a | |
Sasha Koshka | e5d6e03975 | |
Sasha Koshka | 990e60eea4 | |
Sasha Koshka | 4e20726eff | |
Sasha Koshka | 3502da814d | |
Sasha Koshka | 5407e52108 | |
Sasha Koshka | 7bb7111460 | |
Sasha Koshka | e41fd63f35 | |
Sasha Koshka | 58e02dced8 | |
Sasha Koshka | 61d3c14519 | |
Sasha Koshka | cd7a683af9 | |
Sasha Koshka | e1156d65c8 | |
Sasha Koshka | fc4b2eb36d | |
Sasha Koshka | 1c0dee1b95 | |
Sasha Koshka | cc14151a14 | |
Sasha Koshka | 6622799019 | |
Sasha Koshka | f88268bb0e | |
Sasha Koshka | c1046b1bcb | |
Sasha Koshka | 27799a9670 | |
Sasha Koshka | 2bd7d0fad5 | |
Sasha Koshka | e5619ebf07 | |
Sasha Koshka | 399dda75bd | |
Sasha Koshka | 53f78cb0e7 | |
Sasha Koshka | eaee284aaf | |
Sasha Koshka | 4fe778c095 | |
Sasha Koshka | d4b9ffb046 | |
Sasha Koshka | 580b7d2ad0 | |
Sasha Koshka | ff3802ca5e | |
Sasha Koshka | 17fda82bbe | |
Sasha Koshka | 0063afed8c | |
Sasha Koshka | 1323a6c1ca | |
Sasha Koshka | 698414ee65 | |
Sasha Koshka | dbee2ff5a9 | |
Sasha Koshka | afdecc2c8b | |
Sasha Koshka | ac58a43220 | |
Sasha Koshka | 7cdc5868e5 | |
Sasha Koshka | 14080b1f88 | |
Sasha Koshka | a2b1ac0c73 | |
Sasha Koshka | 6276327613 | |
Sasha Koshka | d44e7b51da | |
Sasha Koshka | 785cc2d908 | |
Sasha Koshka | 0bf5c3b86c | |
Sasha Koshka | 6b13e772a9 | |
Sasha Koshka | 427b5e025d | |
Sasha Koshka | 5ca3b80e8e | |
Sasha Koshka | 775390e884 | |
Sasha Koshka | a7de6c7f3b | |
Sasha Koshka | 7d4ddaf387 | |
Sasha Koshka | b9c8350677 | |
Sasha Koshka | ed6de3a36f | |
Sasha Koshka | e16195d274 | |
Sasha Koshka | 0a21f605fb | |
Sasha Koshka | 0cd7fb9be9 | |
Sasha Koshka | c0b205c6f0 | |
Sasha Koshka | 1044c8299a | |
Sasha Koshka | bb50c7d7a7 | |
Sasha Koshka | 9d78a599aa | |
Sasha Koshka | 986315d5db | |
Sasha Koshka | 9e16f7b532 | |
Sasha Koshka | ca86328506 | |
Sasha Koshka | a43f5ce595 | |
Sasha Koshka | 437aef0c27 | |
Sasha Koshka | 5cf0b162c0 | |
Sasha Koshka | 6e4310b9ad | |
Sasha Koshka | 68128c94d8 | |
Sasha Koshka | 4c6f1f80e7 | |
Sasha Koshka | e931717241 | |
Sasha Koshka | bb9c5df088 | |
Sasha Koshka | 407b957687 | |
Sasha Koshka | 99c890e6cd | |
Sasha Koshka | b190f01a71 | |
Sasha Koshka | fa898be046 | |
Sasha Koshka | a51372bd7b | |
Sasha Koshka | 670cf36c14 | |
Sasha Koshka | d67aac3d4f | |
Sasha Koshka | 2987331a31 | |
Sasha Koshka | da47026d1c | |
Sasha Koshka | aed448671b | |
Sasha Koshka | 6db5901247 | |
Sasha Koshka | 8abb45e77a | |
Sasha Koshka | d1fcc6e37f | |
Sasha Koshka | dc077a02ab | |
Sasha Koshka | 43a664009c | |
Sasha Koshka | f21a41982e | |
Sasha Koshka | 34b79ee30d | |
Sasha Koshka | 8db8fab14a | |
Sasha Koshka | cbdebc7f9f | |
Sasha Koshka | 570853890e | |
Sasha Koshka | 260e2b31b6 | |
Sasha Koshka | d633e0f5f6 | |
Sasha Koshka | f377372354 | |
Sasha Koshka | 55c13ebf89 | |
Sasha Koshka | eca75c642b | |
Sasha Koshka | e38e2a47f9 | |
Sasha Koshka | b357768c36 | |
Sasha Koshka | ebefcb03b3 | |
Sasha Koshka | 13518d9ba6 | |
Sasha Koshka | ff51777834 | |
Sasha Koshka | 941f6f6576 | |
Sasha Koshka | 603d029c50 | |
Sasha Koshka | 5c2be06601 | |
Sasha Koshka | 2d0a0cc073 | |
Sasha Koshka | 46a4858597 | |
Sasha Koshka | 6c3230c0f8 | |
Sasha Koshka | 6ede0d0770 | |
Sasha Koshka | 7521808872 | |
Sasha Koshka | bc72333ff0 | |
Sasha Koshka | 7fee67474f | |
Sasha Koshka | 9f70804420 | |
Sasha Koshka | e9dff8ad07 | |
Sasha Koshka | bd636eaa7f | |
Sasha Koshka | 4e488582d0 | |
Sasha Koshka | b8bf5743b4 | |
Sasha Koshka | 8c03b516e3 | |
Sasha Koshka | 50d7d74097 | |
Sasha Koshka | 03dfcf02bf | |
Sasha Koshka | c7cd944ae2 | |
Sasha Koshka | d1b5cd863a | |
Sasha Koshka | e7ec9ad6f3 | |
Sasha Koshka | c1e2bf46a6 | |
Sasha Koshka | ab78bc640d | |
Sasha Koshka | 7b300333cf | |
Sasha Koshka | 53bfc8df68 | |
Sasha Koshka | 719b7b99ac | |
Sasha Koshka | e7ad588fb8 | |
Sasha Koshka | 6406b70077 | |
Sasha Koshka | 6456759bfc | |
Sasha Koshka | 0d4104255c | |
Sasha Koshka | 17422cc054 | |
Sasha Koshka | a16f3c2cd7 | |
Sasha Koshka | 017543aa0f | |
Sasha Koshka | f9e5503320 | |
Sasha Koshka | 8abc4defa7 | |
Sasha Koshka | fc228a13d3 | |
Sasha Koshka | 1ebf5e1103 | |
Sasha Koshka | ab61615018 | |
Sasha Koshka | 39dc09bc4a | |
Sasha Koshka | 0aede3502b | |
Sasha Koshka | 6f15ff3366 | |
Sasha Koshka | 01a0fc1bd3 | |
Sasha Koshka | 02a27447b9 | |
Sasha Koshka | 6a3f45a2e0 | |
Sasha Koshka | 3aa8495873 | |
Sasha Koshka | bdc1109bcf | |
Sasha Koshka | a2c0ff5f4c | |
Sasha Koshka | d710d13f0d | |
Sasha Koshka | fff5ad4d96 | |
Sasha Koshka | 8447b06641 | |
Sasha Koshka | 6a08d0f317 | |
Sasha Koshka | d3d3cddfef | |
Sasha Koshka | 45021b6153 | |
Sasha Koshka | 6638a471c7 | |
Sasha Koshka | 6c8ff55dc1 | |
Sasha Koshka | 7ec5e1ab2a | |
Sasha Koshka | 14802b4b82 | |
Sasha Koshka | f74f6a43f8 | |
Sasha Koshka | 68341517f7 | |
Sasha Koshka | dcc672e2bc | |
Sasha Koshka | d9bddce20b | |
Sasha Koshka | 60aac053fb | |
Sasha Koshka | faf5ebb283 | |
Sasha Koshka | f37101eb9e | |
Sasha Koshka | d475e5e2ec | |
Sasha Koshka | 221647a265 | |
Sasha Koshka | bf667aded9 | |
Sasha Koshka | b4befa5aa5 | |
Sasha Koshka | 0f272f4835 | |
Sasha Koshka | d651570746 | |
Sasha Koshka | 493c5210a7 | |
Sasha Koshka | 0fd56f272c | |
Sasha Koshka | 4c6e01203c | |
Sasha Koshka | b189518c92 | |
Sasha Koshka | 4b788dd783 | |
Sasha Koshka | cdf805dadc | |
Sasha Koshka | 6258c77f86 | |
Sasha Koshka | b90ffeb4fd | |
Sasha Koshka | a4ef28cdd0 | |
Sasha Koshka | c55925d152 | |
Sasha Koshka | 11b680db63 | |
Sasha Koshka | d57db6327d | |
Sasha Koshka | 0d8d2a0190 | |
Sasha Koshka | 6ee3014fda | |
Sasha Koshka | 0ebf0bc814 | |
Sasha Koshka | 40aa1a788b | |
Sasha Koshka | bffdb000ed | |
Sasha Koshka | 5ca9206f65 | |
Sasha Koshka | 1239f4e03d | |
Sasha Koshka | 8aaa017902 | |
Sasha Koshka | 639baecee5 | |
Sasha Koshka | c1b3562d10 | |
Sasha Koshka | ef325d5161 | |
Sasha Koshka | 2f60abdfa3 | |
Sasha Koshka | 1a66224648 | |
Sasha Koshka | 275e113e3b | |
Sasha Koshka | 0015820fac | |
Sasha Koshka | f4799ba03d | |
Sasha Koshka | 14ad35d85c | |
Sasha Koshka | a34e8768ab | |
Sasha Koshka | b08cbea320 | |
Sasha Koshka | 9d84c50db3 | |
Sasha Koshka | 99e029ae09 | |
Sasha Koshka | 5149c27cf3 | |
Sasha Koshka | 7ef95cc751 | |
Sasha Koshka | b09994973c | |
Sasha Koshka | 37048c6759 | |
Sasha Koshka | be45f7ad71 | |
Sasha Koshka | c45268d8c1 | |
Sasha Koshka | 92e5822185 | |
Sasha Koshka | d31aee1ba8 | |
Sasha Koshka | 0f8affd2b2 | |
Sasha Koshka | 3d28ebe4cf | |
Sasha Koshka | 5afbc0e713 | |
Sasha Koshka | b7a7800370 | |
Sasha Koshka | 15fa3b2497 | |
Sasha Koshka | 081b005679 | |
Sasha Koshka | 1be769526d | |
Sasha Koshka | 51084a6cfe | |
Sasha Koshka | 677dca1dbf | |
Sasha Koshka | 9cc9e78504 | |
Sasha Koshka | 5d4a26a877 | |
Sasha Koshka | aaa794ac04 | |
Sasha Koshka | 8658ecd879 | |
Sasha Koshka | 1c28613981 | |
Sasha Koshka | 8e1638e054 | |
Sasha Koshka | aff9aca835 | |
Sasha Koshka | cf672824a6 | |
Sasha Koshka | 04884bd8e3 | |
Sasha Koshka | 305acea285 | |
Sasha Koshka | f3c1c95a57 | |
Sasha Koshka | 423e6869c0 | |
Sasha Koshka | 803812f9c9 | |
Sasha Koshka | c171273240 | |
Sasha Koshka | 11402cfc25 | |
Sasha Koshka | 7e0d64e8bd | |
Sasha Koshka | d38bd1cbf5 | |
Sasha Koshka | 865dd20724 | |
Sasha Koshka | 6c46fc6f7c | |
Sasha Koshka | 0071994ba6 | |
Sasha Koshka | 11c747e225 | |
Sasha Koshka | 61bbe0e346 | |
Sasha Koshka | 1e8bb56b7c | |
Sasha Koshka | ae6cf128f8 | |
Sasha Koshka | ecaad02c0b | |
Sasha Koshka | cad10a1fb1 | |
Sasha Koshka | 912a3f9f66 | |
Sasha Koshka | 531b0ffce9 | |
Sasha Koshka | 9c12cd7e18 | |
Sasha Koshka | 4f6f4e1f1a | |
Sasha Koshka | dc5ddfc0bd | |
Sasha Koshka | 5fc5af92df | |
Sasha Koshka | 90ce0d7281 | |
Sasha Koshka | be286fa86c | |
Sasha Koshka | 252433f13d | |
Sasha Koshka | 165d0835bf | |
Sasha Koshka | 56e11ae1de | |
Sasha Koshka | 1d9fb6024d | |
Sasha Koshka | c13cdd570d | |
Sasha Koshka | 5af8d7fd97 | |
Sasha Koshka | b6eb158964 | |
Sasha Koshka | 6bb5b2d79c | |
Sasha Koshka | 538123dcd5 | |
Sasha Koshka | 38baa97e76 | |
Sasha Koshka | e9e6e4fbe7 | |
Sasha Koshka | 285cb4810f | |
Sasha Koshka | 2cac2b3bd0 | |
Sasha Koshka | 1f2e8aa677 | |
Sasha Koshka | ef59f46559 | |
Sasha Koshka | 829f1525b8 | |
Sasha Koshka | b1d15fb4ec | |
Sasha Koshka | ee45b2fa60 | |
Sasha Koshka | 8dd506a007 | |
Sasha Koshka | de10cde630 | |
Sasha Koshka | 449922851f | |
Sasha Koshka | 26787d8670 | |
Sasha Koshka | cda2d1f0ae | |
Sasha Koshka | 241c297626 | |
Sasha Koshka | 2859dc3313 | |
Sasha Koshka | 7e51dc5e5a | |
Sasha Koshka | 81090267a6 | |
Sasha Koshka | bf2fdb5eaa | |
Sasha Koshka | 211219eb01 | |
Sasha Koshka | 79ab1c8ac0 | |
Sasha Koshka | d167559830 | |
Sasha Koshka | 48237f5687 | |
Sasha Koshka | 0ba3c982c4 | |
Sasha Koshka | c7e44633b1 | |
Sasha Koshka | b575413a0a | |
Sasha Koshka | 29e4a7572b | |
Sasha Koshka | ddb960571f | |
Sasha Koshka | ce1d938f7a | |
Sasha Koshka | 20fa445cdd | |
Sasha Koshka | e966771f5b | |
Sasha Koshka | e9e1ccc35e | |
Sasha Koshka | 0c39c2dd57 | |
Sasha Koshka | f8240fb518 | |
Sasha Koshka | fc0a9292d9 | |
Sasha Koshka | b9cbf83a18 | |
Sasha Koshka | 270b49f825 | |
Sasha Koshka | e3369ab3d4 | |
Sasha Koshka | 50e9c3b1c9 | |
Sasha Koshka | e2e846a0e5 | |
Sasha Koshka | fa934fa485 | |
Sasha Koshka | 56dc9ba54c | |
Sasha Koshka | 7235c86e22 | |
Sasha Koshka | 367aee4570 | |
Sasha Koshka | bd55b6c17d | |
Sasha Koshka | a0e7bf1373 | |
Sasha Koshka | 234503f104 | |
Sasha Koshka | ae551c47ea | |
Sasha Koshka | 0c22977693 | |
Sasha Koshka | 4d87972235 | |
Sasha Koshka | d59b7d812d | |
Sasha Koshka | 09f782953e | |
Sasha Koshka | fa42cf1f5f | |
Sasha Koshka | dcaf9919e4 | |
Sasha Koshka | d18da8b07a | |
Sasha Koshka | 88502cf628 | |
Sasha Koshka | 21abd147bf | |
Sasha Koshka | 4bc8566820 | |
Sasha Koshka | 8ac5108211 | |
Sasha Koshka | 7f0462d588 | |
Sasha Koshka | 82e92f1e2e | |
Sasha Koshka | 9e8e986977 | |
Sasha Koshka | 2d9a941da8 | |
Sasha Koshka | c64ce8da67 | |
Sasha Koshka | a893831a21 | |
Sasha Koshka | 7f1c3ae870 | |
Sasha Koshka | d7a6193c04 | |
Sasha Koshka | a74f9809af | |
Sasha Koshka | f9032a9a95 | |
Sasha Koshka | 0e3de11203 | |
Sasha Koshka | 981c11bd44 | |
Sasha Koshka | dce0321e9b | |
Sasha Koshka | 5e448edb21 | |
Sasha Koshka | c33faa402b | |
Sasha Koshka | 182cb1e35b | |
Sasha Koshka | cfc2b5e130 | |
Sasha Koshka | 6e7cf285cc | |
Sasha Koshka | e3aea7fc9e | |
Sasha Koshka | 5446ffe40b | |
Sasha Koshka | 06e97461fa | |
Sasha Koshka | b38232ee24 | |
Sasha Koshka | 2cd670f4cd | |
Sasha Koshka | c7bebabed5 | |
Sasha Koshka | b15c260dfc | |
Sasha Koshka | 16ce15621e | |
Sasha Koshka | 16a0e76145 | |
Sasha Koshka | ce20b7d02c | |
Sasha Koshka | c5ee7c8cdb | |
Sasha Koshka | 5c7e243566 | |
Sasha Koshka | bec8b817c8 | |
Sasha Koshka | 6cc0f36000 | |
Sasha Koshka | a0e57921a4 | |
Sasha Koshka | 6936353516 | |
Sasha Koshka | 3998d842b1 | |
Sasha Koshka | 0bdbaa39ca | |
Sasha Koshka | f8ebe5b1e4 | |
Sasha Koshka | 8d90dbdc92 | |
Sasha Koshka | 43fea5c8ba | |
Sasha Koshka | 2ff32ca8ea | |
Sasha Koshka | d79701d01b | |
Sasha Koshka | bdf599f93c | |
Sasha Koshka | 8ccaa0faba | |
Sasha Koshka | 83b8040520 | |
Sasha Koshka | 4722656c7d | |
Sasha Koshka | 14d1836209 | |
Sasha Koshka | 36b995c514 | |
Sasha Koshka | 46574cfb10 | |
Sasha Koshka | 8606968c74 | |
Sasha Koshka | 892c74a9da | |
Sasha Koshka | 99942466f8 | |
Sasha Koshka | da6fe2c845 | |
Sasha Koshka | 04d2ea4767 | |
Sasha Koshka | f71f789b60 | |
Sasha Koshka | 8f0f2be9e9 |
|
@ -0,0 +1 @@
|
|||
/build
|
39
README.md
39
README.md
|
@ -1,27 +1,24 @@
|
|||
# ![tomo](assets/banner.png)
|
||||
# ![tomo](assets/screenshot.png)
|
||||
|
||||
This repository is [mirrored on GitHub](https://github.com/sashakoshka/tomo).
|
||||
|
||||
Please note: Tomo is in early development. Some features may not work properly,
|
||||
and its API may change without notice.
|
||||
|
||||
Tomo is a retro-looking GUI toolkit written in pure Go. It is designed with
|
||||
these goals in mind:
|
||||
Tomo is a GUI toolkit written in pure Go with minimal external dependencies. It
|
||||
makes use of Go's unique language features to do more with less.
|
||||
|
||||
- Modularity: the core of Tomo is mostly composed of interfaces—and the
|
||||
overwhelming majority of its code resides in pluggable modules. If you don't
|
||||
need it, then dont import it—and you can be assured it won't be there.
|
||||
- Extendability: during the design of Tomo's API, use cases such as creating
|
||||
custom backends, elements, and layouts were given just as much importance as
|
||||
normal application building. Your custom element is a first-class citizen.
|
||||
- Independence: Tomo is minimally dependent on code outside of the Go
|
||||
standard library. Because of this, the file size of a compiled Tomo application
|
||||
is typically very small.
|
||||
- Frugality: Tomo foregoes things like animations and anti-aliasing in order to
|
||||
use a minimal amount of system resources without even having to touch the GPU.
|
||||
- Consistency: Tomo's design is not only consistent within itself, but also
|
||||
with the Go standard library. If you've worked with Go in the past, Tomo will
|
||||
feel pleasantly familliar.
|
||||
Nasin is an application framework that runs on top of Tomo. It supports plugins
|
||||
which can extend any application with backends, themes, etc.
|
||||
|
||||
You can find out more about how to use it by visiting the examples directory,
|
||||
or pull up its documentation by running `godoc` within the repository. You can
|
||||
also view it on the web on
|
||||
[pkg.go.dev](https://pkg.go.dev/git.tebibyte.media/sashakoshka/tomo).
|
||||
## Usage
|
||||
|
||||
Before you start using Tomo, you need to install a backend plugin. Currently,
|
||||
there is only an X backend. You can run ./scripts/install-backends.sh to install
|
||||
it. It will be placed in `~/.local/lib/nasin/plugins`.
|
||||
|
||||
You can find out more about how to use Tomo and Nasin by visiting the examples
|
||||
directory, or pull up the documentation by running `godoc` within the
|
||||
repository. You can also view it on the web on
|
||||
[pkg.go.dev](https://pkg.go.dev/git.tebibyte.media/tomo/tomo) (although
|
||||
it may be slightly out of date).
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
// Package ability defines extended interfaces that elements can support.
|
||||
package ability
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
|
||||
// Layoutable represents an element that needs to perform layout calculations
|
||||
// before it can draw itself.
|
||||
type Layoutable interface {
|
||||
tomo.Element
|
||||
|
||||
// Layout causes this element to perform a layout operation.
|
||||
Layout ()
|
||||
}
|
||||
|
||||
// Container represents an element capable of containing child elements.
|
||||
type Container interface {
|
||||
tomo.Element
|
||||
Layoutable
|
||||
|
||||
// DrawBackground causes the element to draw its background pattern to
|
||||
// the specified canvas. The bounds of this canvas specify the area that
|
||||
// is actually drawn to, while the Entity bounds specify the actual area
|
||||
// of the element.
|
||||
DrawBackground (art.Canvas)
|
||||
|
||||
// HandleChildMinimumSizeChange is called when a child's minimum size is
|
||||
// changed.
|
||||
HandleChildMinimumSizeChange (child tomo.Element)
|
||||
}
|
||||
|
||||
// Enableable represents an element that can be enabled and disabled. Disabled
|
||||
// elements typically appear greyed out.
|
||||
type Enableable interface {
|
||||
tomo.Element
|
||||
|
||||
// Enabled returns whether or not the element is enabled.
|
||||
Enabled () bool
|
||||
|
||||
// SetEnabled sets whether or not the element is enabled.
|
||||
SetEnabled (bool)
|
||||
}
|
||||
|
||||
// Focusable represents an element that has keyboard navigation support.
|
||||
type Focusable interface {
|
||||
tomo.Element
|
||||
Enableable
|
||||
|
||||
// HandleFocusChange is called when the element is focused or unfocused.
|
||||
HandleFocusChange ()
|
||||
}
|
||||
|
||||
// Selectable represents an element that can be selected. This includes things
|
||||
// like list items, files, etc. The difference between this and Focusable is
|
||||
// that multiple Selectable elements may be selected at the same time, whereas
|
||||
// only one Focusable element may be focused at the same time. Containers who's
|
||||
// purpose is to contain selectable elements can determine when to select them
|
||||
// by implementing MouseTargetContainer and listening for HandleChildMouseDown
|
||||
// events.
|
||||
type Selectable interface {
|
||||
tomo.Element
|
||||
Enableable
|
||||
|
||||
// HandleSelectionChange is called when the element is selected or
|
||||
// deselected.
|
||||
HandleSelectionChange ()
|
||||
}
|
||||
|
||||
// KeyboardTarget represents an element that can receive keyboard input.
|
||||
type KeyboardTarget interface {
|
||||
tomo.Element
|
||||
|
||||
// HandleKeyDown is called when a key is pressed down or repeated while
|
||||
// this element has keyboard focus. It is important to note that not
|
||||
// every key down event is guaranteed to be paired with exactly one key
|
||||
// up event. This is the reason a list of modifier keys held down at the
|
||||
// time of the key press is given.
|
||||
HandleKeyDown (key input.Key, modifiers input.Modifiers)
|
||||
|
||||
// HandleKeyUp is called when a key is released while this element has
|
||||
// keyboard focus.
|
||||
HandleKeyUp (key input.Key, modifiers input.Modifiers)
|
||||
}
|
||||
|
||||
// MouseTarget represents an element that can receive mouse events.
|
||||
type MouseTarget interface {
|
||||
tomo.Element
|
||||
|
||||
// HandleMouseDown is called when a mouse button is pressed down on this
|
||||
// element.
|
||||
HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers)
|
||||
|
||||
// HandleMouseUp is called when a mouse button is released that was
|
||||
// originally pressed down on this element.
|
||||
HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers)
|
||||
}
|
||||
|
||||
// MouseTargetContainer represents an element that wants to know when one
|
||||
// of its children is clicked. Children do not have to implement MouseTarget for
|
||||
// a container satisfying MouseTargetContainer to be notified that they have
|
||||
// been clicked.
|
||||
type MouseTargetContainer interface {
|
||||
Container
|
||||
|
||||
// HandleMouseDown is called when a mouse button is pressed down on a
|
||||
// child element.
|
||||
HandleChildMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
child tomo.Element)
|
||||
|
||||
// HandleMouseUp is called when a mouse button is released that was
|
||||
// originally pressed down on a child element.
|
||||
HandleChildMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
child tomo.Element)
|
||||
}
|
||||
|
||||
// MotionTarget represents an element that can receive mouse motion events.
|
||||
type MotionTarget interface {
|
||||
tomo.Element
|
||||
|
||||
// HandleMotion is called when the mouse is moved over this element,
|
||||
// or the mouse is moving while being held down and originally pressed
|
||||
// down on this element.
|
||||
HandleMotion (position image.Point)
|
||||
}
|
||||
|
||||
// ScrollTarget represents an element that can receive mouse scroll events.
|
||||
type ScrollTarget interface {
|
||||
tomo.Element
|
||||
|
||||
// HandleScroll is called when the mouse is scrolled. The X and Y
|
||||
// direction of the scroll event are passed as deltaX and deltaY.
|
||||
HandleScroll (
|
||||
position image.Point,
|
||||
deltaX, deltaY float64,
|
||||
modifiers input.Modifiers)
|
||||
}
|
||||
|
||||
// Flexible represents an element who's preferred minimum height can change in
|
||||
// response to its width.
|
||||
type Flexible interface {
|
||||
tomo.Element
|
||||
|
||||
// FlexibleHeightFor returns what the element's minimum height would be
|
||||
// if resized to a specified width. This does not actually alter the
|
||||
// state of the element in any way, but it may perform significant work,
|
||||
// so it should be called sparingly.
|
||||
//
|
||||
// It is reccomended that parent containers check for this interface and
|
||||
// take this method's value into account in order to support things like
|
||||
// flow layouts and text wrapping, but it is not absolutely necessary.
|
||||
// The element's MinimumSize method will still return the absolute
|
||||
// minimum size that the element may be resized to.
|
||||
//
|
||||
// It is important to note that if a parent container checks for
|
||||
// flexible chilren, it itself will likely need to be either scrollable
|
||||
// or flexible.
|
||||
FlexibleHeightFor (width int) int
|
||||
}
|
||||
|
||||
// FlexibleContainer represents an element that is capable of containing
|
||||
// flexible children.
|
||||
type FlexibleContainer interface {
|
||||
Container
|
||||
|
||||
// HandleChildFlexibleHeightChange is called when the parameters
|
||||
// affecting a child's flexible height are changed.
|
||||
HandleChildFlexibleHeightChange (child Flexible)
|
||||
}
|
||||
|
||||
// Scrollable represents an element that can be scrolled. It acts as a viewport
|
||||
// through which its contents can be observed.
|
||||
type Scrollable interface {
|
||||
tomo.Element
|
||||
|
||||
// ScrollContentBounds returns the full content size of the element.
|
||||
ScrollContentBounds () image.Rectangle
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's
|
||||
// viewport relative to ScrollBounds.
|
||||
ScrollViewportBounds () image.Rectangle
|
||||
|
||||
// ScrollTo scrolls the viewport to the specified point relative to
|
||||
// ScrollBounds.
|
||||
ScrollTo (position image.Point)
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
ScrollAxes () (horizontal, vertical bool)
|
||||
}
|
||||
|
||||
// ScrollableContainer represents an element that is capable of containing
|
||||
// scrollable children.
|
||||
type ScrollableContainer interface {
|
||||
Container
|
||||
|
||||
// HandleChildScrollBoundsChange is called when the content bounds,
|
||||
// viewport bounds, or scroll axes of a child are changed.
|
||||
HandleChildScrollBoundsChange (child Scrollable)
|
||||
}
|
||||
|
||||
// Collapsible represents an element who's minimum width and height can be
|
||||
// manually resized. Scrollable elements should implement this if possible.
|
||||
type Collapsible interface {
|
||||
tomo.Element
|
||||
|
||||
// Collapse collapses the element's minimum width and height. A value of
|
||||
// zero for either means that the element's normal value is used.
|
||||
Collapse (width, height int)
|
||||
}
|
||||
|
||||
// Themeable represents an element that can modify its appearance to fit within
|
||||
// a theme.
|
||||
type Themeable interface {
|
||||
tomo.Element
|
||||
|
||||
// HandleThemeChange is called whenever the theme is changed.
|
||||
HandleThemeChange ()
|
||||
}
|
||||
|
||||
// Configurable represents an element that can modify its behavior to fit within
|
||||
// a set of configuration parameters.
|
||||
type Configurable interface {
|
||||
tomo.Element
|
||||
|
||||
// HandleConfigChange is called whenever configuration parameters are
|
||||
// changed.
|
||||
HandleConfigChange ()
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "image/color"
|
||||
|
||||
// Beveled is a pattern that has a highlight section and a shadow section.
|
||||
type Beveled [2]Pattern
|
||||
|
||||
// AtWhen satisfies the Pattern interface.
|
||||
func (pattern Beveled) AtWhen (x, y, width, height int) (c color.RGBA) {
|
||||
return QuadBeveled {
|
||||
pattern[0],
|
||||
pattern[1],
|
||||
pattern[1],
|
||||
pattern[0],
|
||||
}.AtWhen(x, y, width, height)
|
||||
}
|
||||
|
||||
// QuadBeveled is like Beveled, but with four sides. A pattern can be specified
|
||||
// for each one.
|
||||
type QuadBeveled [4]Pattern
|
||||
|
||||
// AtWhen satisfies the Pattern interface.
|
||||
func (pattern QuadBeveled) AtWhen (x, y, width, height int) (c color.RGBA) {
|
||||
bottom := y > height / 2
|
||||
right := x > width / 2
|
||||
top := !bottom
|
||||
left := !right
|
||||
side := 0
|
||||
|
||||
switch {
|
||||
case top && left:
|
||||
if x < y { side = 3 } else { side = 0 }
|
||||
|
||||
case top && right:
|
||||
if width - x > y { side = 0 } else { side = 1 }
|
||||
|
||||
case bottom && left:
|
||||
if x < height - y { side = 3 } else { side = 2 }
|
||||
|
||||
case bottom && right:
|
||||
if width - x > height - y { side = 2 } else { side = 1 }
|
||||
|
||||
}
|
||||
|
||||
return pattern[side].AtWhen(x, y, width, height)
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
|
||||
// Bordered is a pattern with a border and a fill.
|
||||
type Bordered struct {
|
||||
Fill Pattern
|
||||
Stroke
|
||||
}
|
||||
|
||||
// AtWhen satisfies the Pattern interface.
|
||||
func (pattern Bordered) AtWhen (x, y, width, height int) (c color.RGBA) {
|
||||
outerBounds := image.Rectangle { Max: image.Point { width, height }}
|
||||
innerBounds := outerBounds.Inset(pattern.Weight)
|
||||
if (image.Point { x, y }).In (innerBounds) {
|
||||
return pattern.Fill.AtWhen (
|
||||
x - pattern.Weight,
|
||||
y - pattern.Weight,
|
||||
innerBounds.Dx(), innerBounds.Dy())
|
||||
} else {
|
||||
return pattern.Stroke.AtWhen(x, y, width, height)
|
||||
}
|
||||
}
|
||||
|
||||
// Stroke represents a stoke that has a weight and a pattern.
|
||||
type Stroke struct {
|
||||
Weight int
|
||||
Pattern
|
||||
}
|
||||
|
||||
type borderInternal struct {
|
||||
weight int
|
||||
stroke Pattern
|
||||
bounds image.Rectangle
|
||||
dx, dy int
|
||||
}
|
||||
|
||||
// MultiBordered is a pattern that allows multiple borders of different lengths
|
||||
// to be inset within one another. The final border is treated as a fill color,
|
||||
// and its weight does not matter.
|
||||
type MultiBordered struct {
|
||||
borders []borderInternal
|
||||
lastWidth, lastHeight int
|
||||
maxBorder int
|
||||
}
|
||||
|
||||
// NewMultiBordered creates a new MultiBordered pattern from the given list of
|
||||
// borders.
|
||||
func NewMultiBordered (borders ...Stroke) (multi *MultiBordered) {
|
||||
internalBorders := make([]borderInternal, len(borders))
|
||||
for index, border := range borders {
|
||||
internalBorders[index].weight = border.Weight
|
||||
internalBorders[index].stroke = border.Pattern
|
||||
}
|
||||
return &MultiBordered { borders: internalBorders }
|
||||
}
|
||||
|
||||
// AtWhen satisfies the Pattern interface.
|
||||
func (multi *MultiBordered) AtWhen (x, y, width, height int) (c color.RGBA) {
|
||||
if multi.lastWidth != width || multi.lastHeight != height {
|
||||
multi.recalculate(width, height)
|
||||
}
|
||||
point := image.Point { x, y }
|
||||
for index := multi.maxBorder; index >= 0; index -- {
|
||||
border := multi.borders[index]
|
||||
if point.In(border.bounds) {
|
||||
return border.stroke.AtWhen (
|
||||
point.X - border.bounds.Min.X,
|
||||
point.Y - border.bounds.Min.Y,
|
||||
border.dx, border.dy)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (multi *MultiBordered) recalculate (width, height int) {
|
||||
bounds := image.Rect (0, 0, width, height)
|
||||
multi.maxBorder = 0
|
||||
for index, border := range multi.borders {
|
||||
multi.maxBorder = index
|
||||
multi.borders[index].bounds = bounds
|
||||
multi.borders[index].dx = bounds.Dx()
|
||||
multi.borders[index].dy = bounds.Dy()
|
||||
bounds = bounds.Inset(border.weight)
|
||||
if bounds.Empty() { break }
|
||||
}
|
||||
}
|
||||
|
||||
// Padded is a pattern that surrounds a central fill pattern with a border that
|
||||
// can have a different width for each side.
|
||||
type Padded struct {
|
||||
Fill Pattern
|
||||
Stroke Pattern
|
||||
Sides []int
|
||||
}
|
||||
|
||||
// AtWhen satisfies the Pattern interface.
|
||||
func (pattern Padded) AtWhen (x, y, width, height int) (c color.RGBA) {
|
||||
innerBounds := image.Rect (
|
||||
pattern.Sides[3], pattern.Sides[0],
|
||||
width - pattern.Sides[1], height - pattern.Sides[2])
|
||||
if (image.Point { x, y }).In (innerBounds) {
|
||||
return pattern.Fill.AtWhen (
|
||||
x - pattern.Sides[3],
|
||||
y - pattern.Sides[0],
|
||||
innerBounds.Dx(), innerBounds.Dy())
|
||||
} else {
|
||||
return pattern.Stroke.AtWhen(x, y, width, height)
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "image/color"
|
||||
|
||||
// Checkered is a pattern that produces a grid of two alternating colors.
|
||||
type Checkered struct {
|
||||
First Pattern
|
||||
Second Pattern
|
||||
CellWidth, CellHeight int
|
||||
}
|
||||
|
||||
// AtWhen satisfies the Pattern interface.
|
||||
func (pattern Checkered) AtWhen (x, y, width, height int) (c color.RGBA) {
|
||||
twidth := pattern.CellWidth * 2
|
||||
theight := pattern.CellHeight * 2
|
||||
x %= twidth
|
||||
y %= theight
|
||||
if x < 0 { x += twidth }
|
||||
if y < 0 { x += theight }
|
||||
|
||||
n := 0
|
||||
if x >= pattern.CellWidth { n ++ }
|
||||
if y >= pattern.CellHeight { n ++ }
|
||||
|
||||
x %= pattern.CellWidth
|
||||
y %= pattern.CellHeight
|
||||
|
||||
if n % 2 == 0 {
|
||||
return pattern.First.AtWhen (
|
||||
x, y, pattern.CellWidth, pattern.CellHeight)
|
||||
} else {
|
||||
return pattern.Second.AtWhen (
|
||||
x, y, pattern.CellWidth, pattern.CellHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// Tiled is a pattern that tiles another pattern accross a grid.
|
||||
type Tiled struct {
|
||||
Pattern
|
||||
CellWidth, CellHeight int
|
||||
}
|
||||
|
||||
// AtWhen satisfies the Pattern interface.
|
||||
func (pattern Tiled) AtWhen (x, y, width, height int) (c color.RGBA) {
|
||||
x %= pattern.CellWidth
|
||||
y %= pattern.CellHeight
|
||||
if x < 0 { x += pattern.CellWidth }
|
||||
if y < 0 { y += pattern.CellHeight }
|
||||
return pattern.Pattern.AtWhen (
|
||||
x, y, pattern.CellWidth, pattern.CellHeight)
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "math"
|
||||
import "image/color"
|
||||
|
||||
// EllipticallyBordered is a pattern with a border and a fill that is elliptical
|
||||
// in shape.
|
||||
type EllipticallyBordered struct {
|
||||
Fill Pattern
|
||||
Stroke
|
||||
}
|
||||
|
||||
// AtWhen satisfies the Pattern interface.
|
||||
func (pattern EllipticallyBordered) AtWhen (x, y, width, height int) (c color.RGBA) {
|
||||
xf := (float64(x) + 0.5) / float64(width ) * 2 - 1
|
||||
yf := (float64(y) + 0.5) / float64(height) * 2 - 1
|
||||
distance := math.Sqrt(xf * xf + yf * yf)
|
||||
|
||||
var radius float64
|
||||
if width < height {
|
||||
// vertical
|
||||
radius = 1 - float64(pattern.Weight * 2) / float64(width)
|
||||
} else {
|
||||
// horizontal
|
||||
radius = 1 - float64(pattern.Weight * 2) / float64(height)
|
||||
}
|
||||
|
||||
if distance < radius {
|
||||
return pattern.Fill.AtWhen(x, y, width, height)
|
||||
} else {
|
||||
return pattern.Stroke.AtWhen(x, y, width, height)
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "math"
|
||||
import "image/color"
|
||||
|
||||
// Dotted is a pattern that produces a grid of circles.
|
||||
type Dotted struct {
|
||||
Background Pattern
|
||||
Foreground Pattern
|
||||
Size int
|
||||
Spacing int
|
||||
}
|
||||
|
||||
// AtWhen satisfies the Pattern interface.
|
||||
func (pattern Dotted) AtWhen (x, y, width, height int) (c color.RGBA) {
|
||||
xm := x % pattern.Spacing
|
||||
ym := y % pattern.Spacing
|
||||
if xm < 0 { xm += pattern.Spacing }
|
||||
if ym < 0 { xm += pattern.Spacing }
|
||||
radius := float64(pattern.Size) / 2
|
||||
spacing := float64(pattern.Spacing) / 2 - 0.5
|
||||
xf := float64(xm) - spacing
|
||||
yf := float64(ym) - spacing
|
||||
|
||||
if math.Sqrt(xf * xf + yf * yf) > radius {
|
||||
return pattern.Background.AtWhen(x, y, width, height)
|
||||
} else {
|
||||
return pattern.Foreground.AtWhen(x, y, width, height)
|
||||
}
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "math"
|
||||
import "image"
|
||||
import "image/color"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
// FillEllipse draws a filled ellipse with the specified pattern.
|
||||
func FillEllipse (
|
||||
destination tomo.Canvas,
|
||||
source Pattern,
|
||||
bounds image.Rectangle,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
bounds = bounds.Canon()
|
||||
data, stride := destination.Buffer()
|
||||
realWidth, realHeight := bounds.Dx(), bounds.Dy()
|
||||
bounds = bounds.Intersect(destination.Bounds()).Canon()
|
||||
if bounds.Empty() { return }
|
||||
updatedRegion = bounds
|
||||
|
||||
width, height := bounds.Dx(), bounds.Dy()
|
||||
for y := 0; y < height; y ++ {
|
||||
for x := 0; x < width; x ++ {
|
||||
xf := (float64(x) + 0.5) / float64(realWidth) - 0.5
|
||||
yf := (float64(y) + 0.5) / float64(realHeight) - 0.5
|
||||
if math.Sqrt(xf * xf + yf * yf) <= 0.5 {
|
||||
data[x + bounds.Min.X + (y + bounds.Min.Y) * stride] =
|
||||
source.AtWhen(x, y, realWidth, realHeight)
|
||||
}
|
||||
}}
|
||||
return
|
||||
}
|
||||
|
||||
// StrokeEllipse draws the outline of an ellipse with the specified line weight
|
||||
// and pattern.
|
||||
func StrokeEllipse (
|
||||
destination tomo.Canvas,
|
||||
source Pattern,
|
||||
weight int,
|
||||
bounds image.Rectangle,
|
||||
) {
|
||||
if weight < 1 { return }
|
||||
|
||||
data, stride := destination.Buffer()
|
||||
bounds = bounds.Canon().Inset(weight - 1)
|
||||
width, height := bounds.Dx(), bounds.Dy()
|
||||
|
||||
context := ellipsePlottingContext {
|
||||
data: data,
|
||||
stride: stride,
|
||||
source: source,
|
||||
width: width,
|
||||
height: height,
|
||||
weight: weight,
|
||||
bounds: bounds,
|
||||
}
|
||||
|
||||
bounds.Max.X -= 1
|
||||
bounds.Max.Y -= 1
|
||||
|
||||
radii := image.Pt (
|
||||
bounds.Dx() / 2,
|
||||
bounds.Dy() / 2)
|
||||
center := bounds.Min.Add(radii)
|
||||
|
||||
x := float64(0)
|
||||
y := float64(radii.Y)
|
||||
|
||||
// region 1 decision parameter
|
||||
decision1 :=
|
||||
float64(radii.Y * radii.Y) -
|
||||
float64(radii.X * radii.X * radii.Y) +
|
||||
(0.25 * float64(radii.X) * float64(radii.X))
|
||||
decisionX := float64(2 * radii.Y * radii.Y * int(x))
|
||||
decisionY := float64(2 * radii.X * radii.X * int(y))
|
||||
|
||||
// draw region 1
|
||||
for decisionX < decisionY {
|
||||
context.plot( int(x) + center.X, int(y) + center.Y)
|
||||
context.plot(-int(x) + center.X, int(y) + center.Y)
|
||||
context.plot( int(x) + center.X, -int(y) + center.Y)
|
||||
context.plot(-int(x) + center.X, -int(y) + center.Y)
|
||||
|
||||
if (decision1 < 0) {
|
||||
x ++
|
||||
decisionX += float64(2 * radii.Y * radii.Y)
|
||||
decision1 += decisionX + float64(radii.Y * radii.Y)
|
||||
} else {
|
||||
x ++
|
||||
y --
|
||||
decisionX += float64(2 * radii.Y * radii.Y)
|
||||
decisionY -= float64(2 * radii.X * radii.X)
|
||||
decision1 +=
|
||||
decisionX - decisionY +
|
||||
float64(radii.Y * radii.Y)
|
||||
}
|
||||
}
|
||||
|
||||
// region 2 decision parameter
|
||||
decision2 :=
|
||||
float64(radii.Y * radii.Y) * (x + 0.5) * (x + 0.5) +
|
||||
float64(radii.X * radii.X) * (y - 1) * (y - 1) -
|
||||
float64(radii.X * radii.X * radii.Y * radii.Y)
|
||||
|
||||
// draw region 2
|
||||
for y >= 0 {
|
||||
context.plot( int(x) + center.X, int(y) + center.Y)
|
||||
context.plot(-int(x) + center.X, int(y) + center.Y)
|
||||
context.plot( int(x) + center.X, -int(y) + center.Y)
|
||||
context.plot(-int(x) + center.X, -int(y) + center.Y)
|
||||
|
||||
if decision2 > 0 {
|
||||
y --
|
||||
decisionY -= float64(2 * radii.X * radii.X)
|
||||
decision2 += float64(radii.X * radii.X) - decisionY
|
||||
} else {
|
||||
y --
|
||||
x ++
|
||||
decisionX += float64(2 * radii.Y * radii.Y)
|
||||
decisionY -= float64(2 * radii.X * radii.X)
|
||||
decision2 +=
|
||||
decisionX - decisionY +
|
||||
float64(radii.X * radii.X)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ellipsePlottingContext struct {
|
||||
data []color.RGBA
|
||||
stride int
|
||||
source Pattern
|
||||
width, height int
|
||||
weight int
|
||||
bounds image.Rectangle
|
||||
}
|
||||
|
||||
func (context ellipsePlottingContext) plot (x, y int) {
|
||||
if (image.Point { x, y }).In(context.bounds) {
|
||||
squareAround (
|
||||
context.data, context.stride, context.source, x, y,
|
||||
context.width, context.height, context.weight)
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "image/color"
|
||||
|
||||
// Gradient is a pattern that interpolates between two colors.
|
||||
type Gradient struct {
|
||||
First Pattern
|
||||
Second Pattern
|
||||
Orientation
|
||||
}
|
||||
|
||||
// AtWhen satisfies the Pattern interface.
|
||||
func (pattern Gradient) AtWhen (x, y, width, height int) (c color.RGBA) {
|
||||
var position float64
|
||||
switch pattern.Orientation {
|
||||
case OrientationVertical:
|
||||
position = float64(y) / float64(height)
|
||||
case OrientationDiagonalRight:
|
||||
position = (float64(width - x) / float64(width) +
|
||||
float64(y) / float64(height)) / 2
|
||||
case OrientationHorizontal:
|
||||
position = float64(x) / float64(width)
|
||||
case OrientationDiagonalLeft:
|
||||
position = (float64(x) / float64(width) +
|
||||
float64(y) / float64(height)) / 2
|
||||
}
|
||||
|
||||
firstColor := pattern.First.AtWhen(x, y, width, height)
|
||||
secondColor := pattern.Second.AtWhen(x, y, width, height)
|
||||
return LerpRGBA(firstColor, secondColor, position)
|
||||
}
|
||||
|
||||
// Lerp linearally interpolates between two integer values.
|
||||
func Lerp (first, second int, fac float64) (n int) {
|
||||
return int(float64(first) * (1 - fac) + float64(second) * fac)
|
||||
}
|
||||
|
||||
// LerpRGBA linearally interpolates between two color.RGBA values.
|
||||
func LerpRGBA (first, second color.RGBA, fac float64) (c color.RGBA) {
|
||||
return color.RGBA {
|
||||
R: uint8(Lerp(int(first.R), int(second.R), fac)),
|
||||
G: uint8(Lerp(int(first.G), int(second.G), fac)),
|
||||
B: uint8(Lerp(int(first.G), int(second.B), fac)),
|
||||
}
|
||||
}
|
143
artist/line.go
143
artist/line.go
|
@ -1,143 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
// TODO: draw thick lines more efficiently
|
||||
|
||||
// Line draws a line from one point to another with the specified weight and
|
||||
// pattern.
|
||||
func Line (
|
||||
destination tomo.Canvas,
|
||||
source Pattern,
|
||||
weight int,
|
||||
min image.Point,
|
||||
max image.Point,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
|
||||
updatedRegion = image.Rectangle { Min: min, Max: max }.Canon()
|
||||
updatedRegion.Max.X ++
|
||||
updatedRegion.Max.Y ++
|
||||
width := updatedRegion.Dx()
|
||||
height := updatedRegion.Dy()
|
||||
|
||||
if abs(max.Y - min.Y) <
|
||||
abs(max.X - min.X) {
|
||||
|
||||
if max.X < min.X {
|
||||
temp := min
|
||||
min = max
|
||||
max = temp
|
||||
}
|
||||
lineLow(destination, source, weight, min, max, width, height)
|
||||
} else {
|
||||
|
||||
if max.Y < min.Y {
|
||||
temp := min
|
||||
min = max
|
||||
max = temp
|
||||
}
|
||||
lineHigh(destination, source, weight, min, max, width, height)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lineLow (
|
||||
destination tomo.Canvas,
|
||||
source Pattern,
|
||||
weight int,
|
||||
min image.Point,
|
||||
max image.Point,
|
||||
width, height int,
|
||||
) {
|
||||
data, stride := destination.Buffer()
|
||||
bounds := destination.Bounds()
|
||||
|
||||
deltaX := max.X - min.X
|
||||
deltaY := max.Y - min.Y
|
||||
yi := 1
|
||||
|
||||
if deltaY < 0 {
|
||||
yi = -1
|
||||
deltaY *= -1
|
||||
}
|
||||
|
||||
D := (2 * deltaY) - deltaX
|
||||
y := min.Y
|
||||
|
||||
for x := min.X; x < max.X; x ++ {
|
||||
if !(image.Point { x, y }).In(bounds) { break }
|
||||
squareAround(data, stride, source, x, y, width, height, weight)
|
||||
// data[x + y * stride] = source.AtWhen(x, y, width, height)
|
||||
if D > 0 {
|
||||
y += yi
|
||||
D += 2 * (deltaY - deltaX)
|
||||
} else {
|
||||
D += 2 * deltaY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lineHigh (
|
||||
destination tomo.Canvas,
|
||||
source Pattern,
|
||||
weight int,
|
||||
min image.Point,
|
||||
max image.Point,
|
||||
width, height int,
|
||||
) {
|
||||
data, stride := destination.Buffer()
|
||||
bounds := destination.Bounds()
|
||||
|
||||
deltaX := max.X - min.X
|
||||
deltaY := max.Y - min.Y
|
||||
xi := 1
|
||||
|
||||
if deltaX < 0 {
|
||||
xi = -1
|
||||
deltaX *= -1
|
||||
}
|
||||
|
||||
D := (2 * deltaX) - deltaY
|
||||
x := min.X
|
||||
|
||||
for y := min.Y; y < max.Y; y ++ {
|
||||
if !(image.Point { x, y }).In(bounds) { break }
|
||||
squareAround(data, stride, source, x, y, width, height, weight)
|
||||
// data[x + y * stride] = source.AtWhen(x, y, width, height)
|
||||
if D > 0 {
|
||||
x += xi
|
||||
D += 2 * (deltaX - deltaY)
|
||||
} else {
|
||||
D += 2 * deltaX
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func abs (in int) (out int) {
|
||||
if in < 0 { in *= -1}
|
||||
out = in
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: this method of doing things sucks and can cause a segfault. we should
|
||||
// not be doing it this way
|
||||
func squareAround (
|
||||
data []color.RGBA,
|
||||
stride int,
|
||||
source Pattern,
|
||||
x, y, patternWidth, patternHeight, diameter int,
|
||||
) {
|
||||
minY := y - diameter + 1
|
||||
minX := x - diameter + 1
|
||||
maxY := y + diameter
|
||||
maxX := x + diameter
|
||||
for y = minY; y < maxY; y ++ {
|
||||
for x = minX; x < maxX; x ++ {
|
||||
data[x + y * stride] =
|
||||
source.AtWhen(x, y, patternWidth, patternHeight)
|
||||
}}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "image/color"
|
||||
|
||||
// Noisy is a pattern that randomly interpolates between two patterns in a
|
||||
// deterministic fashion.
|
||||
type Noisy struct {
|
||||
Low Pattern
|
||||
High Pattern
|
||||
Seed uint32
|
||||
Harsh bool
|
||||
}
|
||||
|
||||
// AtWhen satisfies the pattern interface.
|
||||
func (pattern Noisy) AtWhen (x, y, width, height int) (c color.RGBA) {
|
||||
// FIXME: this will occasionally generate "clumps"
|
||||
special := uint32(x + y * 348905)
|
||||
special += (pattern.Seed + 1) * 15485863
|
||||
random := (special * special * special % 2038074743)
|
||||
fac := float64(random) / 2038074743.0
|
||||
|
||||
if pattern.Harsh {
|
||||
if fac > 0.5 {
|
||||
return pattern.High.AtWhen(x, y, width, height)
|
||||
} else {
|
||||
return pattern.Low.AtWhen(x, y, width, height)
|
||||
}
|
||||
} else {
|
||||
return LerpRGBA (
|
||||
pattern.Low.AtWhen(x, y, width, height),
|
||||
pattern.High.AtWhen(x, y, width, height), fac)
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "image/color"
|
||||
|
||||
// Pattern is capable of generating a pattern pixel by pixel.
|
||||
type Pattern interface {
|
||||
// AtWhen returns the color of the pixel located at (x, y) relative to
|
||||
// the origin point of the pattern (0, 0), when the pattern has the
|
||||
// specified width and height. Patterns may ignore the width and height
|
||||
// parameters, but it may be useful for some patterns such as gradients.
|
||||
AtWhen (x, y, width, height int) (color.RGBA)
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
// Paste transfers one canvas onto another, offset by the specified point.
|
||||
func Paste (
|
||||
destination tomo.Canvas,
|
||||
source tomo.Canvas,
|
||||
offset image.Point,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
dstData, dstStride := destination.Buffer()
|
||||
srcData, srcStride := source.Buffer()
|
||||
|
||||
sourceBounds :=
|
||||
source.Bounds().Canon().
|
||||
Intersect(destination.Bounds().Sub(offset))
|
||||
if sourceBounds.Empty() { return }
|
||||
|
||||
updatedRegion = sourceBounds.Add(offset)
|
||||
for y := sourceBounds.Min.Y; y < sourceBounds.Max.Y; y ++ {
|
||||
for x := sourceBounds.Min.X; x < sourceBounds.Max.X; x ++ {
|
||||
dstData[x + offset.X + (y + offset.Y) * dstStride] =
|
||||
srcData[x + y * srcStride]
|
||||
}}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// FillRectangle draws a filled rectangle with the specified pattern.
|
||||
func FillRectangle (
|
||||
destination tomo.Canvas,
|
||||
source Pattern,
|
||||
bounds image.Rectangle,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
data, stride := destination.Buffer()
|
||||
realBounds := bounds
|
||||
bounds = bounds.Canon().Intersect(destination.Bounds()).Canon()
|
||||
if bounds.Empty() { return }
|
||||
updatedRegion = bounds
|
||||
|
||||
realWidth, realHeight := realBounds.Dx(), realBounds.Dy()
|
||||
patternOffset := realBounds.Min.Sub(bounds.Min)
|
||||
|
||||
width, height := bounds.Dx(), bounds.Dy()
|
||||
for y := 0; y < height; y ++ {
|
||||
for x := 0; x < width; x ++ {
|
||||
data[x + bounds.Min.X + (y + bounds.Min.Y) * stride] =
|
||||
source.AtWhen (
|
||||
x - patternOffset.X, y - patternOffset.Y,
|
||||
realWidth, realHeight)
|
||||
}}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// StrokeRectangle draws the outline of a rectangle with the specified line
|
||||
// weight and pattern.
|
||||
func StrokeRectangle (
|
||||
destination tomo.Canvas,
|
||||
source Pattern,
|
||||
weight int,
|
||||
bounds image.Rectangle,
|
||||
) {
|
||||
bounds = bounds.Canon()
|
||||
insetBounds := bounds.Inset(weight)
|
||||
if insetBounds.Empty() {
|
||||
FillRectangle(destination, source, bounds)
|
||||
return
|
||||
}
|
||||
|
||||
// top
|
||||
FillRectangle (destination, source, image.Rect (
|
||||
bounds.Min.X, bounds.Min.Y,
|
||||
bounds.Max.X, insetBounds.Min.Y))
|
||||
|
||||
// bottom
|
||||
FillRectangle (destination, source, image.Rect (
|
||||
bounds.Min.X, insetBounds.Max.Y,
|
||||
bounds.Max.X, bounds.Max.Y))
|
||||
|
||||
// left
|
||||
FillRectangle (destination, source, image.Rect (
|
||||
bounds.Min.X, insetBounds.Min.Y,
|
||||
insetBounds.Min.X, insetBounds.Max.Y))
|
||||
|
||||
// right
|
||||
FillRectangle (destination, source, image.Rect (
|
||||
insetBounds.Max.X, insetBounds.Min.Y,
|
||||
bounds.Max.X, insetBounds.Max.Y))
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "image/color"
|
||||
|
||||
// Orientation specifies an eight-way pattern orientation.
|
||||
type Orientation int
|
||||
|
||||
const (
|
||||
OrientationVertical Orientation = iota
|
||||
OrientationDiagonalRight
|
||||
OrientationHorizontal
|
||||
OrientationDiagonalLeft
|
||||
)
|
||||
|
||||
// Split is a pattern that is divided in half between two sub-patterns.
|
||||
type Split struct {
|
||||
First Pattern
|
||||
Second Pattern
|
||||
Orientation
|
||||
}
|
||||
|
||||
// AtWhen satisfies the Pattern interface.
|
||||
func (pattern Split) AtWhen (x, y, width, height int) (c color.RGBA) {
|
||||
var first bool
|
||||
switch pattern.Orientation {
|
||||
case OrientationVertical:
|
||||
first = x < width / 2
|
||||
case OrientationDiagonalRight:
|
||||
first = float64(x) / float64(width) +
|
||||
float64(y) / float64(height) < 1
|
||||
case OrientationHorizontal:
|
||||
first = y < height / 2
|
||||
case OrientationDiagonalLeft:
|
||||
first = float64(width - x) / float64(width) +
|
||||
float64(y) / float64(height) < 1
|
||||
}
|
||||
|
||||
if first {
|
||||
return pattern.First.AtWhen(x, y, width, height)
|
||||
} else {
|
||||
return pattern.Second.AtWhen(x, y, width, height)
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "image/color"
|
||||
|
||||
// Striped is a pattern that produces stripes of two alternating colors.
|
||||
type Striped struct {
|
||||
First Stroke
|
||||
Second Stroke
|
||||
Orientation
|
||||
}
|
||||
|
||||
// AtWhen satisfies the Pattern interface.
|
||||
func (pattern Striped) AtWhen (x, y, width, height int) (c color.RGBA) {
|
||||
position := 0
|
||||
switch pattern.Orientation {
|
||||
case OrientationVertical:
|
||||
position = x
|
||||
case OrientationDiagonalRight:
|
||||
position = x + y
|
||||
case OrientationHorizontal:
|
||||
position = y
|
||||
case OrientationDiagonalLeft:
|
||||
position = x - y
|
||||
}
|
||||
|
||||
phase := pattern.First.Weight + pattern.Second.Weight
|
||||
position %= phase
|
||||
if position < 0 {
|
||||
position += phase
|
||||
}
|
||||
|
||||
if position < pattern.First.Weight {
|
||||
return pattern.First.AtWhen(x, y, width, height)
|
||||
} else {
|
||||
return pattern.Second.AtWhen(x, y, width, height)
|
||||
}
|
||||
}
|
356
artist/text.go
356
artist/text.go
|
@ -1,356 +0,0 @@
|
|||
package artist
|
||||
|
||||
// import "fmt"
|
||||
import "image"
|
||||
import "unicode"
|
||||
import "image/draw"
|
||||
import "golang.org/x/image/font"
|
||||
import "golang.org/x/image/math/fixed"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
type characterLayout struct {
|
||||
x int
|
||||
character rune
|
||||
}
|
||||
|
||||
type wordLayout struct {
|
||||
position image.Point
|
||||
width int
|
||||
spaceAfter int
|
||||
breaksAfter int
|
||||
text []characterLayout
|
||||
whitespace []characterLayout
|
||||
}
|
||||
|
||||
// Align specifies a text alignment method.
|
||||
type Align int
|
||||
|
||||
const (
|
||||
// AlignLeft aligns the start of each line to the beginning point
|
||||
// of each dot.
|
||||
AlignLeft Align = iota
|
||||
AlignRight
|
||||
AlignCenter
|
||||
AlignJustify
|
||||
)
|
||||
|
||||
// TextDrawer is a struct that is capable of efficient rendering of wrapped
|
||||
// text, and calculating text bounds. It avoids doing redundant work
|
||||
// automatically.
|
||||
type TextDrawer struct {
|
||||
runes []rune
|
||||
face font.Face
|
||||
width int
|
||||
height int
|
||||
align Align
|
||||
wrap bool
|
||||
cut bool
|
||||
|
||||
layout []wordLayout
|
||||
layoutClean bool
|
||||
layoutBounds image.Rectangle
|
||||
}
|
||||
|
||||
// SetText sets the text of the text drawer.
|
||||
func (drawer *TextDrawer) SetText (runes []rune) {
|
||||
// if drawer.runes == runes { return }
|
||||
drawer.runes = runes
|
||||
drawer.layoutClean = false
|
||||
}
|
||||
|
||||
// SetFace sets the font face of the text drawer.
|
||||
func (drawer *TextDrawer) SetFace (face font.Face) {
|
||||
if drawer.face == face { return }
|
||||
drawer.face = face
|
||||
drawer.layoutClean = false
|
||||
}
|
||||
|
||||
// SetMaxWidth sets a maximum width for the text drawer, and recalculates the
|
||||
// layout if needed. If zero is given, there will be no width limit and the text
|
||||
// will not wrap.
|
||||
func (drawer *TextDrawer) SetMaxWidth (width int) {
|
||||
if drawer.width == width { return }
|
||||
drawer.width = width
|
||||
drawer.wrap = width != 0
|
||||
drawer.layoutClean = false
|
||||
}
|
||||
|
||||
// SetMaxHeight sets a maximum height for the text drawer. Lines that are
|
||||
// entirely below this height will not be drawn, and lines that are on the cusp
|
||||
// of this maximum height will be clipped at the point that they cross it.
|
||||
func (drawer *TextDrawer) SetMaxHeight (height int) {
|
||||
if drawer.height == height { return }
|
||||
drawer.height = height
|
||||
drawer.cut = height != 0
|
||||
drawer.layoutClean = false
|
||||
}
|
||||
|
||||
// SetAlignment specifies how the drawer should align its text. For this to have
|
||||
// an effect, a maximum width must have been set.
|
||||
func (drawer *TextDrawer) SetAlignment (align Align) {
|
||||
if drawer.align == align { return }
|
||||
drawer.align = align
|
||||
drawer.layoutClean = false
|
||||
}
|
||||
|
||||
// Draw draws the drawer's text onto the specified canvas at the given offset.
|
||||
func (drawer *TextDrawer) Draw (
|
||||
destination tomo.Canvas,
|
||||
source Pattern,
|
||||
offset image.Point,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
wrappedSource := WrappedPattern {
|
||||
Pattern: source,
|
||||
Width: 0,
|
||||
Height: 0, // TODO: choose a better width and height
|
||||
}
|
||||
|
||||
if !drawer.layoutClean { drawer.recalculate() }
|
||||
// TODO: reimplement a version of draw mask that takes in a pattern and
|
||||
// only draws to a tomo.Canvas.
|
||||
for _, word := range drawer.layout {
|
||||
for _, character := range word.text {
|
||||
destinationRectangle,
|
||||
mask, maskPoint, _, ok := drawer.face.Glyph (
|
||||
fixed.P (
|
||||
offset.X + word.position.X + character.x,
|
||||
offset.Y + word.position.Y),
|
||||
character.character)
|
||||
if !ok { continue }
|
||||
|
||||
// FIXME: clip destination rectangle if we are on the cusp of
|
||||
// the maximum height.
|
||||
|
||||
draw.DrawMask (
|
||||
destination,
|
||||
destinationRectangle,
|
||||
wrappedSource, image.Point { },
|
||||
mask, maskPoint,
|
||||
draw.Over)
|
||||
|
||||
updatedRegion = updatedRegion.Union(destinationRectangle)
|
||||
}}
|
||||
return
|
||||
}
|
||||
|
||||
// LayoutBounds returns a semantic bounding box for text to be used to determine
|
||||
// an offset for drawing. If a maximum width or height has been set, those will
|
||||
// be used as the width and height of the bounds respectively. The origin point
|
||||
// (0, 0) of the returned bounds will be equivalent to the baseline at the start
|
||||
// of the first line. As such, the minimum of the bounds will be negative.
|
||||
func (drawer *TextDrawer) LayoutBounds () (bounds image.Rectangle) {
|
||||
if !drawer.layoutClean { drawer.recalculate() }
|
||||
bounds = drawer.layoutBounds
|
||||
return
|
||||
}
|
||||
|
||||
// Em returns the width of an emspace.
|
||||
func (drawer *TextDrawer) Em () (width fixed.Int26_6) {
|
||||
if drawer.face == nil { return }
|
||||
width, _ = drawer.face.GlyphAdvance('M')
|
||||
return
|
||||
}
|
||||
|
||||
// LineHeight returns the height of one line.
|
||||
func (drawer *TextDrawer) LineHeight () (height fixed.Int26_6) {
|
||||
if drawer.face == nil { return }
|
||||
metrics := drawer.face.Metrics()
|
||||
height = metrics.Height
|
||||
return
|
||||
}
|
||||
|
||||
// ReccomendedHeightFor returns the reccomended max height if the text were to
|
||||
// have its maximum width set to the given width. This does not alter the
|
||||
// drawer's state.
|
||||
func (drawer *TextDrawer) ReccomendedHeightFor (width int) (height int) {
|
||||
if !drawer.layoutClean { drawer.recalculate() }
|
||||
metrics := drawer.face.Metrics()
|
||||
dot := fixed.Point26_6 { 0, metrics.Height }
|
||||
for _, word := range drawer.layout {
|
||||
if word.width + dot.X.Round() > width {
|
||||
dot.Y += metrics.Height
|
||||
dot.X = 0
|
||||
}
|
||||
dot.X += fixed.I(word.width + word.spaceAfter)
|
||||
if word.breaksAfter > 0 {
|
||||
dot.Y += fixed.I(word.breaksAfter).Mul(metrics.Height)
|
||||
dot.X = 0
|
||||
}
|
||||
}
|
||||
|
||||
return dot.Y.Round()
|
||||
}
|
||||
|
||||
// PositionOf returns the position of the character at the specified index
|
||||
// relative to the baseline.
|
||||
func (drawer *TextDrawer) PositionOf (index int) (position image.Point) {
|
||||
if !drawer.layoutClean { drawer.recalculate() }
|
||||
index ++
|
||||
for _, word := range drawer.layout {
|
||||
position = word.position
|
||||
for _, character := range word.text {
|
||||
index --
|
||||
position.X = word.position.X + character.x
|
||||
if index < 1 { return }
|
||||
}
|
||||
for _, character := range word.whitespace {
|
||||
index --
|
||||
position.X = word.position.X + character.x
|
||||
if index < 1 { return }
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Length returns the amount of runes in the drawer's text.
|
||||
func (drawer *TextDrawer) Length () (length int) {
|
||||
return len(drawer.runes)
|
||||
}
|
||||
|
||||
func (drawer *TextDrawer) recalculate () {
|
||||
drawer.layoutClean = true
|
||||
drawer.layout = nil
|
||||
drawer.layoutBounds = image.Rectangle { }
|
||||
if drawer.runes == nil { return }
|
||||
if drawer.face == nil { return }
|
||||
|
||||
metrics := drawer.face.Metrics()
|
||||
dot := fixed.Point26_6 { 0, 0 }
|
||||
index := 0
|
||||
horizontalExtent := 0
|
||||
currentCharacterX := fixed.Int26_6(0)
|
||||
|
||||
previousCharacter := rune(-1)
|
||||
for index < len(drawer.runes) {
|
||||
word := wordLayout { }
|
||||
word.position.X = dot.X.Round()
|
||||
word.position.Y = dot.Y.Round()
|
||||
|
||||
// process a word
|
||||
currentCharacterX = 0
|
||||
wordWidth := fixed.Int26_6(0)
|
||||
for index < len(drawer.runes) && !unicode.IsSpace(drawer.runes[index]) {
|
||||
character := drawer.runes[index]
|
||||
_, advance, ok := drawer.face.GlyphBounds(character)
|
||||
index ++
|
||||
if !ok { continue }
|
||||
|
||||
word.text = append(word.text, characterLayout {
|
||||
x: currentCharacterX.Round(),
|
||||
character: character,
|
||||
})
|
||||
|
||||
dot.X += advance
|
||||
wordWidth += advance
|
||||
currentCharacterX += advance
|
||||
if dot.X.Round () > horizontalExtent {
|
||||
horizontalExtent = dot.X.Round()
|
||||
}
|
||||
if previousCharacter >= 0 {
|
||||
dot.X += drawer.face.Kern (
|
||||
previousCharacter,
|
||||
character)
|
||||
}
|
||||
previousCharacter = character
|
||||
}
|
||||
word.width = wordWidth.Round()
|
||||
|
||||
// detect if the word that was just processed goes out of
|
||||
// bounds, and if it does, wrap it
|
||||
if drawer.wrap &&
|
||||
word.width + word.position.X > drawer.width &&
|
||||
word.position.X > 0 {
|
||||
|
||||
word.position.Y += metrics.Height.Round()
|
||||
word.position.X = 0
|
||||
dot.Y += metrics.Height
|
||||
dot.X = wordWidth
|
||||
}
|
||||
|
||||
// process whitespace, going onto a new line if there is a
|
||||
// newline character
|
||||
spaceWidth := fixed.Int26_6(0)
|
||||
for index < len(drawer.runes) && unicode.IsSpace(drawer.runes[index]) {
|
||||
character := drawer.runes[index]
|
||||
_, advance, ok := drawer.face.GlyphBounds(character)
|
||||
index ++
|
||||
if !ok { continue }
|
||||
word.whitespace = append(word.whitespace, characterLayout {
|
||||
x: currentCharacterX.Round(),
|
||||
character: character,
|
||||
})
|
||||
spaceWidth += advance
|
||||
currentCharacterX += advance
|
||||
|
||||
if character == '\n' {
|
||||
dot.Y += metrics.Height
|
||||
dot.X = 0
|
||||
word.breaksAfter ++
|
||||
break
|
||||
} else {
|
||||
dot.X += advance
|
||||
if previousCharacter >= 0 {
|
||||
dot.X += drawer.face.Kern (
|
||||
previousCharacter,
|
||||
character)
|
||||
}
|
||||
}
|
||||
previousCharacter = character
|
||||
}
|
||||
word.spaceAfter = spaceWidth.Round()
|
||||
|
||||
// add the word to the layout
|
||||
drawer.layout = append(drawer.layout, word)
|
||||
|
||||
// if there is a set maximum height, and we have crossed it,
|
||||
// stop processing more words. and remove any words that have
|
||||
// also crossed the line.
|
||||
if
|
||||
drawer.cut &&
|
||||
(dot.Y - metrics.Ascent - metrics.Descent).Round() >
|
||||
drawer.height {
|
||||
|
||||
for
|
||||
index := len(drawer.layout) - 1;
|
||||
index >= 0; index -- {
|
||||
|
||||
if drawer.layout[index].position.Y < dot.Y.Round() {
|
||||
break
|
||||
}
|
||||
drawer.layout = drawer.layout[:index]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// add a little null to the last character
|
||||
if len(drawer.layout) > 0 {
|
||||
lastWord := &drawer.layout[len(drawer.layout) - 1]
|
||||
lastWord.whitespace = append (
|
||||
lastWord.whitespace,
|
||||
characterLayout {
|
||||
x: currentCharacterX.Round(),
|
||||
})
|
||||
}
|
||||
|
||||
if drawer.wrap {
|
||||
drawer.layoutBounds.Max.X = drawer.width
|
||||
} else {
|
||||
drawer.layoutBounds.Max.X = horizontalExtent
|
||||
}
|
||||
|
||||
if drawer.cut {
|
||||
drawer.layoutBounds.Min.Y = 0 - metrics.Ascent.Round()
|
||||
drawer.layoutBounds.Max.Y = drawer.height - metrics.Ascent.Round()
|
||||
} else {
|
||||
drawer.layoutBounds.Min.Y = 0 - metrics.Ascent.Round()
|
||||
drawer.layoutBounds.Max.Y = dot.Y.Round() + metrics.Descent.Round()
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// for each line, calculate the bounds as if the words are left aligned,
|
||||
// and then at the end of the process go through each line and re-align
|
||||
// everything. this will make the process far simpler.
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
|
||||
// Texture is a struct that allows an image to be converted into a tiling
|
||||
// texture pattern.
|
||||
type Texture struct {
|
||||
data []color.RGBA
|
||||
width, height int
|
||||
}
|
||||
|
||||
// NewTexture converts an image into a texture.
|
||||
func NewTexture (source image.Image) (texture Texture) {
|
||||
bounds := source.Bounds()
|
||||
texture.width = bounds.Dx()
|
||||
texture.height = bounds.Dy()
|
||||
texture.data = make([]color.RGBA, texture.width * texture.height)
|
||||
|
||||
index := 0
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
|
||||
r, g, b, a := source.At(x, y).RGBA()
|
||||
texture.data[index] = color.RGBA {
|
||||
uint8(r >> 8),
|
||||
uint8(g >> 8),
|
||||
uint8(b >> 8),
|
||||
uint8(a >> 8),
|
||||
}
|
||||
index ++
|
||||
}}
|
||||
return
|
||||
}
|
||||
|
||||
// AtWhen returns the color at the specified x and y coordinates, wrapped to the
|
||||
// image's width. the width and height are ignored.
|
||||
func (texture Texture) AtWhen (x, y, width, height int) (pixel color.RGBA) {
|
||||
x %= texture.width
|
||||
y %= texture.height
|
||||
if x < 0 { x += texture.width }
|
||||
if y < 0 { y += texture.height }
|
||||
return texture.data[x + y * texture.width]
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
|
||||
// Uniform is an infinite-sized pattern of uniform color. It implements the
|
||||
// Pattern, color.Color, color.Model, and image.Image interfaces.
|
||||
type Uniform color.RGBA
|
||||
|
||||
// NewUniform returns a new Uniform image of the given color.
|
||||
func NewUniform (c color.Color) (uniform Uniform) {
|
||||
r, g, b, a := c.RGBA()
|
||||
uniform.R = uint8(r >> 8)
|
||||
uniform.G = uint8(g >> 8)
|
||||
uniform.B = uint8(b >> 8)
|
||||
uniform.A = uint8(a >> 8)
|
||||
return
|
||||
}
|
||||
|
||||
// ColorModel satisfies the image.Image interface.
|
||||
func (uniform Uniform) ColorModel () (model color.Model) {
|
||||
return uniform
|
||||
}
|
||||
|
||||
// Convert satisfies the color.Model interface.
|
||||
func (uniform Uniform) Convert (in color.Color) (c color.Color) {
|
||||
return color.RGBA(uniform)
|
||||
}
|
||||
|
||||
// Bounds satisfies the image.Image interface.
|
||||
func (uniform Uniform) Bounds () (rectangle image.Rectangle) {
|
||||
rectangle.Min = image.Point { -1e9, -1e9 }
|
||||
rectangle.Max = image.Point { 1e9, 1e9 }
|
||||
return
|
||||
}
|
||||
|
||||
// At satisfies the image.Image interface.
|
||||
func (uniform Uniform) At (x, y int) (c color.Color) {
|
||||
return color.RGBA(uniform)
|
||||
}
|
||||
|
||||
// AtWhen satisfies the Pattern interface.
|
||||
func (uniform Uniform) AtWhen (x, y, width, height int) (c color.RGBA) {
|
||||
return color.RGBA(uniform)
|
||||
}
|
||||
|
||||
// RGBA satisfies the color.Color interface.
|
||||
func (uniform Uniform) RGBA () (r, g, b, a uint32) {
|
||||
return color.RGBA(uniform).RGBA()
|
||||
}
|
||||
|
||||
// Opaque scans the entire image and reports whether it is fully opaque.
|
||||
func (uniform Uniform) Opaque () (opaque bool) {
|
||||
return uniform.A == 0xFF
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package artist
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
|
||||
// WrappedPattern is a pattern that is able to behave like an image.Image.
|
||||
type WrappedPattern struct {
|
||||
Pattern
|
||||
Width, Height int
|
||||
}
|
||||
|
||||
// At satisfies the image.Image interface.
|
||||
func (pattern WrappedPattern) At (x, y int) (c color.Color) {
|
||||
return pattern.Pattern.AtWhen(x, y, pattern.Width, pattern.Height)
|
||||
}
|
||||
|
||||
// Bounds satisfies the image.Image interface.
|
||||
func (pattern WrappedPattern) Bounds () (rectangle image.Rectangle) {
|
||||
rectangle.Min = image.Point { -1e9, -1e9 }
|
||||
rectangle.Max = image.Point { 1e9, 1e9 }
|
||||
return
|
||||
}
|
||||
|
||||
// ColorModel satisfies the image.Image interface.
|
||||
func (pattern WrappedPattern) ColorModel () (model color.Model) {
|
||||
return color.RGBAModel
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
65
backend.go
65
backend.go
|
@ -1,13 +1,13 @@
|
|||
package tomo
|
||||
|
||||
import "errors"
|
||||
import "image"
|
||||
|
||||
// Backend represents a connection to a display server, or something similar.
|
||||
// It is capable of managing an event loop, and creating windows.
|
||||
type Backend interface {
|
||||
// Run runs the backend's event loop. It must block until the backend
|
||||
// experiences a fatal error, or Stop() is called.
|
||||
Run () (err error)
|
||||
Run () error
|
||||
|
||||
// Stop stops the backend's event loop.
|
||||
Stop ()
|
||||
|
@ -16,45 +16,36 @@ type Backend interface {
|
|||
// possible. This method must be safe to call from other threads.
|
||||
Do (callback func ())
|
||||
|
||||
// NewWindow creates a new window with the specified width and height,
|
||||
// and returns a struct representing it that fulfills the Window
|
||||
// interface.
|
||||
NewWindow (width, height int) (window Window, err error)
|
||||
// NewEntity creates a new entity for the specified element.
|
||||
NewEntity (owner Element) Entity
|
||||
|
||||
// Copy puts data into the clipboard.
|
||||
Copy (Data)
|
||||
|
||||
// Paste returns the data currently in the clipboard.
|
||||
Paste (accept []Mime) (Data)
|
||||
// NewWindow creates a new window within the specified bounding
|
||||
// rectangle. The position on screen may be overridden by the backend or
|
||||
// operating system.
|
||||
NewWindow (bounds image.Rectangle) (MainWindow, error)
|
||||
|
||||
// SetTheme sets the theme of all open windows.
|
||||
SetTheme (Theme)
|
||||
|
||||
// SetConfig sets the configuration of all open windows.
|
||||
SetConfig (Config)
|
||||
}
|
||||
|
||||
// BackendFactory represents a function capable of constructing a backend
|
||||
// struct. Any connections should be initialized within this function. If there
|
||||
// any errors encountered during this process, the function should immediately
|
||||
// stop, clean up any resources, and return an error.
|
||||
type BackendFactory func () (backend Backend, err error)
|
||||
var backend Backend
|
||||
|
||||
// RegisterBackend registers a backend factory. When an application calls
|
||||
// tomo.Run(), the first registered backend that does not throw an error will be
|
||||
// used.
|
||||
func RegisterBackend (factory BackendFactory) {
|
||||
factories = append(factories, factory)
|
||||
// GetBackend returns the currently running backend.
|
||||
func GetBackend () Backend {
|
||||
return backend
|
||||
}
|
||||
|
||||
var factories []BackendFactory
|
||||
|
||||
func instantiateBackend () (backend Backend, err error) {
|
||||
// find a suitable backend
|
||||
for _, factory := range factories {
|
||||
backend, err = factory()
|
||||
if err == nil && backend != nil { return }
|
||||
}
|
||||
|
||||
// if none were found, but there was no error produced, produce an
|
||||
// error
|
||||
if err == nil {
|
||||
err = errors.New("no available backends")
|
||||
}
|
||||
|
||||
return
|
||||
// SetBackend sets the currently running backend. The backend can only be set
|
||||
// once—if there already is one then this function will do nothing.
|
||||
func SetBackend (b Backend) {
|
||||
if backend != nil { return }
|
||||
backend = b
|
||||
}
|
||||
|
||||
// Bounds creates a rectangle from an x, y, width, and height.
|
||||
func Bounds (x, y, width, height int) image.Rectangle {
|
||||
return image.Rect(x, y, x + width, y + height)
|
||||
}
|
||||
|
|
|
@ -1,338 +0,0 @@
|
|||
package x
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
import "github.com/jezek/xgbutil"
|
||||
import "github.com/jezek/xgb/xproto"
|
||||
import "github.com/jezek/xgbutil/xevent"
|
||||
|
||||
type scrollSum struct {
|
||||
x, y int
|
||||
}
|
||||
|
||||
const scrollDistance = 16
|
||||
|
||||
func (sum *scrollSum) add (button xproto.Button, window *Window, state uint16) {
|
||||
shift :=
|
||||
(state & xproto.ModMaskShift) > 0 ||
|
||||
(state & window.backend.modifierMasks.shiftLock) > 0
|
||||
if shift {
|
||||
switch button {
|
||||
case 4:
|
||||
sum.x -= scrollDistance
|
||||
case 5:
|
||||
sum.x += scrollDistance
|
||||
case 6:
|
||||
sum.y -= scrollDistance
|
||||
case 7:
|
||||
sum.y += scrollDistance
|
||||
}
|
||||
} else {
|
||||
switch button {
|
||||
case 4:
|
||||
sum.y -= scrollDistance
|
||||
case 5:
|
||||
sum.y += scrollDistance
|
||||
case 6:
|
||||
sum.x -= scrollDistance
|
||||
case 7:
|
||||
sum.x += scrollDistance
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (window *Window) handleExpose (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.ExposeEvent,
|
||||
) {
|
||||
_ = window.compressExpose(*event.ExposeEvent)
|
||||
window.redrawChildEntirely()
|
||||
}
|
||||
|
||||
func (window *Window) handleConfigureNotify (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.ConfigureNotifyEvent,
|
||||
) {
|
||||
if window.child == nil { return }
|
||||
|
||||
configureEvent := *event.ConfigureNotifyEvent
|
||||
|
||||
newWidth := int(configureEvent.Width)
|
||||
newHeight := int(configureEvent.Height)
|
||||
sizeChanged :=
|
||||
window.metrics.width != newWidth ||
|
||||
window.metrics.height != newHeight
|
||||
window.metrics.width = newWidth
|
||||
window.metrics.height = newHeight
|
||||
|
||||
if sizeChanged {
|
||||
configureEvent = window.compressConfigureNotify(configureEvent)
|
||||
window.metrics.width = int(configureEvent.Width)
|
||||
window.metrics.height = int(configureEvent.Height)
|
||||
window.reallocateCanvas()
|
||||
window.resizeChildToFit()
|
||||
|
||||
if !window.exposeEventFollows(configureEvent) {
|
||||
window.redrawChildEntirely()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) exposeEventFollows (event xproto.ConfigureNotifyEvent) (found bool) {
|
||||
nextEvents := xevent.Peek(window.backend.connection)
|
||||
if len(nextEvents) > 0 {
|
||||
untypedEvent := nextEvents[0]
|
||||
if untypedEvent.Err == nil {
|
||||
typedEvent, ok :=
|
||||
untypedEvent.Event.(xproto.ConfigureNotifyEvent)
|
||||
|
||||
if ok && typedEvent.Window == event.Window {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (window *Window) modifiersFromState (
|
||||
state uint16,
|
||||
) (
|
||||
modifiers tomo.Modifiers,
|
||||
) {
|
||||
return tomo.Modifiers {
|
||||
Shift:
|
||||
(state & xproto.ModMaskShift) > 0 ||
|
||||
(state & window.backend.modifierMasks.shiftLock) > 0,
|
||||
Control: (state & xproto.ModMaskControl) > 0,
|
||||
Alt: (state & window.backend.modifierMasks.alt) > 0,
|
||||
Meta: (state & window.backend.modifierMasks.meta) > 0,
|
||||
Super: (state & window.backend.modifierMasks.super) > 0,
|
||||
Hyper: (state & window.backend.modifierMasks.hyper) > 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) handleKeyPress (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.KeyPressEvent,
|
||||
) {
|
||||
if window.child == nil { return }
|
||||
|
||||
keyEvent := *event.KeyPressEvent
|
||||
key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
|
||||
modifiers := window.modifiersFromState(keyEvent.State)
|
||||
modifiers.NumberPad = numberPad
|
||||
|
||||
if key == tomo.KeyTab && modifiers.Alt {
|
||||
if child, ok := window.child.(tomo.Focusable); ok {
|
||||
direction := tomo.KeynavDirectionForward
|
||||
if modifiers.Shift {
|
||||
direction = tomo.KeynavDirectionBackward
|
||||
}
|
||||
|
||||
if !child.HandleFocus(direction) {
|
||||
child.HandleUnfocus()
|
||||
}
|
||||
}
|
||||
} else if child, ok := window.child.(tomo.KeyboardTarget); ok {
|
||||
child.HandleKeyDown(key, modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) handleKeyRelease (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.KeyReleaseEvent,
|
||||
) {
|
||||
if window.child == nil { return }
|
||||
|
||||
keyEvent := *event.KeyReleaseEvent
|
||||
|
||||
// do not process this event if it was generated from a key repeat
|
||||
nextEvents := xevent.Peek(window.backend.connection)
|
||||
if len(nextEvents) > 0 {
|
||||
untypedEvent := nextEvents[0]
|
||||
if untypedEvent.Err == nil {
|
||||
typedEvent, ok :=
|
||||
untypedEvent.Event.(xproto.KeyReleaseEvent)
|
||||
|
||||
if ok && typedEvent.Detail == keyEvent.Detail &&
|
||||
typedEvent.Event == keyEvent.Event &&
|
||||
typedEvent.State == keyEvent.State {
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
|
||||
modifiers := window.modifiersFromState(keyEvent.State)
|
||||
modifiers.NumberPad = numberPad
|
||||
|
||||
if child, ok := window.child.(tomo.KeyboardTarget); ok {
|
||||
child.HandleKeyUp(key, modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) handleButtonPress (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.ButtonPressEvent,
|
||||
) {
|
||||
if window.child == nil { return }
|
||||
|
||||
if child, ok := window.child.(tomo.MouseTarget); ok {
|
||||
buttonEvent := *event.ButtonPressEvent
|
||||
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 {
|
||||
sum := scrollSum { }
|
||||
sum.add(buttonEvent.Detail, window, buttonEvent.State)
|
||||
window.compressScrollSum(buttonEvent, &sum)
|
||||
child.HandleMouseScroll (
|
||||
int(buttonEvent.EventX),
|
||||
int(buttonEvent.EventY),
|
||||
float64(sum.x), float64(sum.y))
|
||||
} else {
|
||||
child.HandleMouseDown (
|
||||
int(buttonEvent.EventX),
|
||||
int(buttonEvent.EventY),
|
||||
tomo.Button(buttonEvent.Detail))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (window *Window) handleButtonRelease (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.ButtonReleaseEvent,
|
||||
) {
|
||||
if window.child == nil { return }
|
||||
|
||||
if child, ok := window.child.(tomo.MouseTarget); ok {
|
||||
buttonEvent := *event.ButtonReleaseEvent
|
||||
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
|
||||
child.HandleMouseUp (
|
||||
int(buttonEvent.EventX),
|
||||
int(buttonEvent.EventY),
|
||||
tomo.Button(buttonEvent.Detail))
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) handleMotionNotify (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.MotionNotifyEvent,
|
||||
) {
|
||||
if window.child == nil { return }
|
||||
|
||||
if child, ok := window.child.(tomo.MouseTarget); ok {
|
||||
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
|
||||
child.HandleMouseMove (
|
||||
int(motionEvent.EventX),
|
||||
int(motionEvent.EventY))
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) compressExpose (
|
||||
firstEvent xproto.ExposeEvent,
|
||||
) (
|
||||
lastEvent xproto.ExposeEvent,
|
||||
) {
|
||||
window.backend.connection.Sync()
|
||||
xevent.Read(window.backend.connection, false)
|
||||
lastEvent = firstEvent
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
||||
if untypedEvent.Err != nil { continue }
|
||||
|
||||
typedEvent, ok := untypedEvent.Event.(xproto.ExposeEvent)
|
||||
if !ok { continue }
|
||||
|
||||
// FIXME: union all areas into the last event
|
||||
if firstEvent.Window == typedEvent.Window {
|
||||
lastEvent = typedEvent
|
||||
defer func (index int) {
|
||||
xevent.DequeueAt(window.backend.connection, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (window *Window) compressConfigureNotify (
|
||||
firstEvent xproto.ConfigureNotifyEvent,
|
||||
) (
|
||||
lastEvent xproto.ConfigureNotifyEvent,
|
||||
) {
|
||||
window.backend.connection.Sync()
|
||||
xevent.Read(window.backend.connection, false)
|
||||
lastEvent = firstEvent
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
||||
if untypedEvent.Err != nil { continue }
|
||||
|
||||
typedEvent, ok := untypedEvent.Event.(xproto.ConfigureNotifyEvent)
|
||||
if !ok { continue }
|
||||
|
||||
if firstEvent.Event == typedEvent.Event &&
|
||||
firstEvent.Window == typedEvent.Window {
|
||||
|
||||
lastEvent = typedEvent
|
||||
defer func (index int) {
|
||||
xevent.DequeueAt(window.backend.connection, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (window *Window) compressScrollSum (
|
||||
firstEvent xproto.ButtonPressEvent,
|
||||
sum *scrollSum,
|
||||
) {
|
||||
window.backend.connection.Sync()
|
||||
xevent.Read(window.backend.connection, false)
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
||||
if untypedEvent.Err != nil { continue }
|
||||
|
||||
typedEvent, ok := untypedEvent.Event.(xproto.ButtonPressEvent)
|
||||
if !ok { continue }
|
||||
|
||||
if firstEvent.Event == typedEvent.Event &&
|
||||
typedEvent.Detail >= 4 &&
|
||||
typedEvent.Detail <= 7 {
|
||||
|
||||
sum.add(typedEvent.Detail, window, typedEvent.State)
|
||||
defer func (index int) {
|
||||
xevent.DequeueAt(window.backend.connection, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (window *Window) compressMotionNotify (
|
||||
firstEvent xproto.MotionNotifyEvent,
|
||||
) (
|
||||
lastEvent xproto.MotionNotifyEvent,
|
||||
) {
|
||||
window.backend.connection.Sync()
|
||||
xevent.Read(window.backend.connection, false)
|
||||
lastEvent = firstEvent
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
||||
if untypedEvent.Err != nil { continue }
|
||||
|
||||
typedEvent, ok := untypedEvent.Event.(xproto.MotionNotifyEvent)
|
||||
if !ok { continue }
|
||||
|
||||
if firstEvent.Event == typedEvent.Event {
|
||||
lastEvent = typedEvent
|
||||
defer func (index int) {
|
||||
xevent.DequeueAt(window.backend.connection, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -1,314 +0,0 @@
|
|||
package x
|
||||
|
||||
import "image"
|
||||
import "github.com/jezek/xgb/xproto"
|
||||
import "github.com/jezek/xgbutil/ewmh"
|
||||
import "github.com/jezek/xgbutil/icccm"
|
||||
import "github.com/jezek/xgbutil/xevent"
|
||||
import "github.com/jezek/xgbutil/xwindow"
|
||||
import "github.com/jezek/xgbutil/xgraphics"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
type Window struct {
|
||||
backend *Backend
|
||||
xWindow *xwindow.Window
|
||||
xCanvas *xgraphics.Image
|
||||
canvas tomo.BasicCanvas
|
||||
child tomo.Element
|
||||
onClose func ()
|
||||
skipChildDrawCallback bool
|
||||
|
||||
metrics struct {
|
||||
width int
|
||||
height int
|
||||
}
|
||||
}
|
||||
|
||||
func (backend *Backend) NewWindow (
|
||||
width, height int,
|
||||
) (
|
||||
output tomo.Window,
|
||||
err error,
|
||||
) {
|
||||
if backend == nil { panic("nil backend") }
|
||||
|
||||
window := &Window { backend: backend }
|
||||
|
||||
window.xWindow, err = xwindow.Generate(backend.connection)
|
||||
if err != nil { return }
|
||||
window.xWindow.Create (
|
||||
backend.connection.RootWin(),
|
||||
0, 0, width, height, 0)
|
||||
err = window.xWindow.Listen (
|
||||
xproto.EventMaskExposure,
|
||||
xproto.EventMaskStructureNotify,
|
||||
xproto.EventMaskPointerMotion,
|
||||
xproto.EventMaskKeyPress,
|
||||
xproto.EventMaskKeyRelease,
|
||||
xproto.EventMaskButtonPress,
|
||||
xproto.EventMaskButtonRelease)
|
||||
if err != nil { return }
|
||||
|
||||
window.xWindow.WMGracefulClose (func (xWindow *xwindow.Window) {
|
||||
window.Close()
|
||||
})
|
||||
|
||||
xevent.ExposeFun(window.handleExpose).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.ConfigureNotifyFun(window.handleConfigureNotify).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.KeyPressFun(window.handleKeyPress).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.KeyReleaseFun(window.handleKeyRelease).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.ButtonPressFun(window.handleButtonPress).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.ButtonReleaseFun(window.handleButtonRelease).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.MotionNotifyFun(window.handleMotionNotify).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
|
||||
window.metrics.width = width
|
||||
window.metrics.height = height
|
||||
window.childMinimumSizeChangeCallback(8, 8)
|
||||
|
||||
window.reallocateCanvas()
|
||||
|
||||
backend.windows[window.xWindow.Id] = window
|
||||
output = window
|
||||
return
|
||||
}
|
||||
|
||||
func (window *Window) Adopt (child tomo.Element) {
|
||||
// disown previous child
|
||||
if window.child != nil {
|
||||
window.child.OnDamage(nil)
|
||||
window.child.OnMinimumSizeChange(nil)
|
||||
}
|
||||
if previousChild, ok := window.child.(tomo.Flexible); ok {
|
||||
previousChild.OnFlexibleHeightChange(nil)
|
||||
}
|
||||
if previousChild, ok := window.child.(tomo.Focusable); ok {
|
||||
previousChild.OnFocusRequest(nil)
|
||||
previousChild.OnFocusMotionRequest(nil)
|
||||
if previousChild.Focused() {
|
||||
previousChild.HandleUnfocus()
|
||||
}
|
||||
}
|
||||
|
||||
// adopt new child
|
||||
window.child = child
|
||||
if newChild, ok := child.(tomo.Flexible); ok {
|
||||
newChild.OnFlexibleHeightChange(window.resizeChildToFit)
|
||||
}
|
||||
if newChild, ok := child.(tomo.Focusable); ok {
|
||||
newChild.OnFocusRequest(window.childSelectionRequestCallback)
|
||||
}
|
||||
if child != nil {
|
||||
child.OnDamage(window.childDrawCallback)
|
||||
child.OnMinimumSizeChange (func () {
|
||||
window.childMinimumSizeChangeCallback (
|
||||
child.MinimumSize())
|
||||
})
|
||||
window.resizeChildToFit()
|
||||
window.childMinimumSizeChangeCallback(child.MinimumSize())
|
||||
window.redrawChildEntirely()
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) Child () (child tomo.Element) {
|
||||
child = window.child
|
||||
return
|
||||
}
|
||||
|
||||
func (window *Window) SetTitle (title string) {
|
||||
ewmh.WmNameSet (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
title)
|
||||
}
|
||||
|
||||
func (window *Window) SetIcon (sizes []image.Image) {
|
||||
wmIcons := []ewmh.WmIcon { }
|
||||
|
||||
for _, icon := range sizes {
|
||||
width := icon.Bounds().Max.X
|
||||
height := icon.Bounds().Max.Y
|
||||
wmIcon := ewmh.WmIcon {
|
||||
Width: uint(width),
|
||||
Height: uint(height),
|
||||
Data: make ([]uint, width * height),
|
||||
}
|
||||
|
||||
// manually convert image data beacuse of course we have to do
|
||||
// this
|
||||
index := 0
|
||||
for y := 0; y < height; y ++ {
|
||||
for x := 0; x < width; x ++ {
|
||||
r, g, b, a := icon.At(x, y).RGBA()
|
||||
r >>= 8
|
||||
g >>= 8
|
||||
b >>= 8
|
||||
a >>= 8
|
||||
wmIcon.Data[index] =
|
||||
(uint(a) << 24) |
|
||||
(uint(r) << 16) |
|
||||
(uint(g) << 8) |
|
||||
(uint(b) << 0)
|
||||
index ++
|
||||
}}
|
||||
|
||||
wmIcons = append(wmIcons, wmIcon)
|
||||
}
|
||||
|
||||
ewmh.WmIconSet (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
wmIcons)
|
||||
}
|
||||
|
||||
func (window *Window) Show () {
|
||||
if window.child == nil {
|
||||
window.xCanvas.For (func (x, y int) xgraphics.BGRA {
|
||||
return xgraphics.BGRA { }
|
||||
})
|
||||
|
||||
window.pushRegion(window.xCanvas.Bounds())
|
||||
}
|
||||
|
||||
window.xWindow.Map()
|
||||
}
|
||||
|
||||
func (window *Window) Hide () {
|
||||
window.xWindow.Unmap()
|
||||
}
|
||||
|
||||
func (window *Window) Close () {
|
||||
delete(window.backend.windows, window.xWindow.Id)
|
||||
if window.onClose != nil { window.onClose() }
|
||||
xevent.Detach(window.xWindow.X, window.xWindow.Id)
|
||||
window.xWindow.Destroy()
|
||||
}
|
||||
|
||||
func (window *Window) OnClose (callback func ()) {
|
||||
window.onClose = callback
|
||||
}
|
||||
|
||||
func (window *Window) reallocateCanvas () {
|
||||
window.canvas = tomo.NewBasicCanvas (
|
||||
window.metrics.width,
|
||||
window.metrics.height)
|
||||
if window.xCanvas != nil {
|
||||
window.xCanvas.Destroy()
|
||||
}
|
||||
window.xCanvas = xgraphics.New (
|
||||
window.backend.connection,
|
||||
image.Rect (
|
||||
0, 0,
|
||||
window.metrics.width,
|
||||
window.metrics.height))
|
||||
window.xCanvas.CreatePixmap()
|
||||
}
|
||||
|
||||
func (window *Window) redrawChildEntirely () {
|
||||
window.pushRegion(window.paste(window.child))
|
||||
|
||||
}
|
||||
|
||||
func (window *Window) resizeChildToFit () {
|
||||
window.skipChildDrawCallback = true
|
||||
if child, ok := window.child.(tomo.Flexible); ok {
|
||||
minimumHeight := child.FlexibleHeightFor(window.metrics.width)
|
||||
minimumWidth, _ := child.MinimumSize()
|
||||
|
||||
icccm.WmNormalHintsSet (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
&icccm.NormalHints {
|
||||
Flags: icccm.SizeHintPMinSize,
|
||||
MinWidth: uint(minimumWidth),
|
||||
MinHeight: uint(minimumHeight),
|
||||
})
|
||||
|
||||
if window.metrics.height >= minimumHeight &&
|
||||
window.metrics.width >= minimumWidth {
|
||||
window.child.DrawTo(window.canvas)
|
||||
}
|
||||
} else {
|
||||
window.child.DrawTo(window.canvas)
|
||||
}
|
||||
window.skipChildDrawCallback = false
|
||||
}
|
||||
|
||||
func (window *Window) childDrawCallback (region tomo.Canvas) {
|
||||
if window.skipChildDrawCallback { return }
|
||||
window.pushRegion(window.paste(region))
|
||||
}
|
||||
|
||||
func (window *Window) paste (canvas tomo.Canvas) (updatedRegion image.Rectangle) {
|
||||
data, stride := canvas.Buffer()
|
||||
bounds := canvas.Bounds().Intersect(window.xCanvas.Bounds())
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
|
||||
rgba := data[x + y * stride]
|
||||
index := x * 4 + y * window.xCanvas.Stride
|
||||
window.xCanvas.Pix[index + 0] = rgba.B
|
||||
window.xCanvas.Pix[index + 1] = rgba.G
|
||||
window.xCanvas.Pix[index + 2] = rgba.R
|
||||
window.xCanvas.Pix[index + 3] = rgba.A
|
||||
}}
|
||||
|
||||
return bounds
|
||||
}
|
||||
|
||||
func (window *Window) childMinimumSizeChangeCallback (width, height int) {
|
||||
icccm.WmNormalHintsSet (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
&icccm.NormalHints {
|
||||
Flags: icccm.SizeHintPMinSize,
|
||||
MinWidth: uint(width),
|
||||
MinHeight: uint(height),
|
||||
})
|
||||
newWidth := window.metrics.width
|
||||
newHeight := window.metrics.height
|
||||
if newWidth < width { newWidth = width }
|
||||
if newHeight < height { newHeight = height }
|
||||
if newWidth != window.metrics.width ||
|
||||
newHeight != window.metrics.height {
|
||||
window.xWindow.Resize(newWidth, newHeight)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) childSelectionRequestCallback () (granted bool) {
|
||||
if child, ok := window.child.(tomo.Focusable); ok {
|
||||
child.HandleFocus(tomo.KeynavDirectionNeutral)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (window *Window) childSelectionMotionRequestCallback (
|
||||
direction tomo.KeynavDirection,
|
||||
) (
|
||||
granted bool,
|
||||
) {
|
||||
if child, ok := window.child.(tomo.Focusable); ok {
|
||||
if !child.HandleFocus(direction) {
|
||||
child.HandleUnfocus()
|
||||
}
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (window *Window) pushRegion (region image.Rectangle) {
|
||||
if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") }
|
||||
image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image)
|
||||
if ok {
|
||||
image.XDraw()
|
||||
image.XExpPaint (
|
||||
window.xWindow.Id,
|
||||
image.Bounds().Min.X,
|
||||
image.Bounds().Min.Y)
|
||||
}
|
||||
}
|
103
backends/x/x.go
103
backends/x/x.go
|
@ -1,103 +0,0 @@
|
|||
package x
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
import "github.com/jezek/xgbutil"
|
||||
import "github.com/jezek/xgb/xproto"
|
||||
import "github.com/jezek/xgbutil/xevent"
|
||||
|
||||
// Backend is an instance of an X backend.
|
||||
type Backend struct {
|
||||
connection *xgbutil.XUtil
|
||||
|
||||
doChannel chan(func ())
|
||||
|
||||
modifierMasks struct {
|
||||
capsLock uint16
|
||||
shiftLock uint16
|
||||
numLock uint16
|
||||
modeSwitch uint16
|
||||
|
||||
alt uint16
|
||||
meta uint16
|
||||
super uint16
|
||||
hyper uint16
|
||||
}
|
||||
|
||||
windows map[xproto.Window] *Window
|
||||
}
|
||||
|
||||
// NewBackend instantiates an X backend.
|
||||
func NewBackend () (output tomo.Backend, err error) {
|
||||
backend := &Backend {
|
||||
windows: map[xproto.Window] *Window { },
|
||||
doChannel: make(chan func (), 0),
|
||||
}
|
||||
|
||||
// connect to X
|
||||
backend.connection, err = xgbutil.NewConn()
|
||||
if err != nil { return }
|
||||
backend.initializeKeymapInformation()
|
||||
|
||||
output = backend
|
||||
return
|
||||
}
|
||||
|
||||
// Run runs the backend's event loop. This method will not exit until Stop() is
|
||||
// called, or the backend experiences a fatal error.
|
||||
func (backend *Backend) Run () (err error) {
|
||||
backend.assert()
|
||||
pingBefore,
|
||||
pingAfter,
|
||||
pingQuit := xevent.MainPing(backend.connection)
|
||||
for {
|
||||
select {
|
||||
case <- pingBefore:
|
||||
<- pingAfter
|
||||
case callback := <- backend.doChannel:
|
||||
callback()
|
||||
case <- pingQuit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop gracefully closes the connection and stops the event loop.
|
||||
func (backend *Backend) Stop () {
|
||||
backend.assert()
|
||||
for _, window := range backend.windows {
|
||||
window.Close()
|
||||
}
|
||||
xevent.Quit(backend.connection)
|
||||
backend.connection.Conn().Close()
|
||||
}
|
||||
|
||||
// Do executes the specified callback within the main thread as soon as
|
||||
// possible. This function can be safely called from other threads.
|
||||
func (backend *Backend) Do (callback func ()) {
|
||||
backend.assert()
|
||||
backend.doChannel <- callback
|
||||
}
|
||||
|
||||
// Copy puts data into the clipboard. This method is not yet implemented and
|
||||
// will do nothing!
|
||||
func (backend *Backend) Copy (data tomo.Data) {
|
||||
backend.assert()
|
||||
// TODO
|
||||
}
|
||||
|
||||
// Paste returns the data currently in the clipboard. This method may
|
||||
// return nil. This method is not yet implemented and will do nothing!
|
||||
func (backend *Backend) Paste (accept []tomo.Mime) (data tomo.Data) {
|
||||
backend.assert()
|
||||
// TODO
|
||||
return
|
||||
}
|
||||
|
||||
func (backend *Backend) assert () {
|
||||
if backend == nil { panic("nil backend") }
|
||||
}
|
||||
|
||||
func init () {
|
||||
tomo.RegisterBackend(NewBackend)
|
||||
}
|
72
canvas.go
72
canvas.go
|
@ -1,72 +0,0 @@
|
|||
package tomo
|
||||
|
||||
import "image"
|
||||
import "image/draw"
|
||||
import "image/color"
|
||||
|
||||
// Canvas is like draw.Image but is also able to return a raw pixel buffer for
|
||||
// more efficient drawing. This interface can be easily satisfied using a
|
||||
// BasicCanvas struct.
|
||||
type Canvas interface {
|
||||
draw.Image
|
||||
Buffer () (data []color.RGBA, stride int)
|
||||
}
|
||||
|
||||
// BasicCanvas is a general purpose implementation of tomo.Canvas.
|
||||
type BasicCanvas struct {
|
||||
pix []color.RGBA
|
||||
stride int
|
||||
rect image.Rectangle
|
||||
}
|
||||
|
||||
// NewBasicCanvas creates a new basic canvas with the specified width and
|
||||
// height, allocating a buffer for it.
|
||||
func NewBasicCanvas (width, height int) (canvas BasicCanvas) {
|
||||
canvas.pix = make([]color.RGBA, height * width)
|
||||
canvas.stride = width
|
||||
canvas.rect = image.Rect(0, 0, width, height)
|
||||
return
|
||||
}
|
||||
|
||||
// you know what it do
|
||||
func (canvas BasicCanvas) Bounds () (bounds image.Rectangle) {
|
||||
return canvas.rect
|
||||
}
|
||||
|
||||
// you know what it do
|
||||
func (canvas BasicCanvas) At (x, y int) (color.Color) {
|
||||
if !image.Pt(x, y).In(canvas.rect) { return nil }
|
||||
return canvas.pix[x + y * canvas.stride]
|
||||
}
|
||||
|
||||
// you know what it do
|
||||
func (canvas BasicCanvas) ColorModel () (model color.Model) {
|
||||
return color.RGBAModel
|
||||
}
|
||||
|
||||
// you know what it do
|
||||
func (canvas BasicCanvas) Set (x, y int, c color.Color) {
|
||||
if !image.Pt(x, y).In(canvas.rect) { return }
|
||||
r, g, b, a := c.RGBA()
|
||||
canvas.pix[x + y * canvas.stride] = color.RGBA {
|
||||
R: uint8(r >> 8),
|
||||
G: uint8(g >> 8),
|
||||
B: uint8(b >> 8),
|
||||
A: uint8(a >> 8),
|
||||
}
|
||||
}
|
||||
|
||||
// you know what it do
|
||||
func (canvas BasicCanvas) Buffer () (data []color.RGBA, stride int) {
|
||||
return canvas.pix, canvas.stride
|
||||
}
|
||||
|
||||
// Cut returns a sub-canvas of a given canvas.
|
||||
func Cut (canvas Canvas, bounds image.Rectangle) (reduced BasicCanvas) {
|
||||
// println(canvas.Bounds().String(), bounds.String())
|
||||
bounds = bounds.Intersect(canvas.Bounds())
|
||||
if bounds.Empty() { return }
|
||||
reduced.rect = bounds
|
||||
reduced.pix, reduced.stride = canvas.Buffer()
|
||||
return
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package tomo
|
||||
|
||||
import "time"
|
||||
|
||||
// Config can return global configuration parameters.
|
||||
type Config interface {
|
||||
// ScrollVelocity returns how many pixels should be scrolled every time
|
||||
// a scroll button is pressed.
|
||||
ScrollVelocity () int
|
||||
|
||||
// DoubleClickDelay returns the maximum delay between two clicks for
|
||||
// them to be registered as a double click.
|
||||
DoubleClickDelay () time.Duration
|
||||
}
|
20
data.go
20
data.go
|
@ -1,20 +0,0 @@
|
|||
package tomo
|
||||
|
||||
import "io"
|
||||
|
||||
// Data represents arbitrary polymorphic data that can be used for data transfer
|
||||
// between applications.
|
||||
type Data map[Mime] io.ReadCloser
|
||||
|
||||
// Mime represents a MIME type.
|
||||
type Mime struct {
|
||||
// Type is the first half of the MIME type, and Subtype is the second
|
||||
// half. The separating slash is not included in either. For example,
|
||||
// text/html becomes:
|
||||
// Mime { Type: "text", Subtype: "html" }
|
||||
Type, Subtype string
|
||||
}
|
||||
|
||||
var MimePlain = Mime { "text", "plain" }
|
||||
|
||||
var MimeFile = Mime { "text", "uri-list" }
|
|
@ -0,0 +1,57 @@
|
|||
// Package data provides operations to deal with arbitrary data and MIME types.
|
||||
package data
|
||||
|
||||
import "io"
|
||||
import "bytes"
|
||||
|
||||
// Data represents arbitrary polymorphic data that can be used for data transfer
|
||||
// between applications.
|
||||
type Data map[Mime] io.ReadSeekCloser
|
||||
|
||||
// Mime represents a MIME type.
|
||||
type Mime struct {
|
||||
// Type is the first half of the MIME type, and Subtype is the second
|
||||
// half. The separating slash is not included in either. For example,
|
||||
// text/html becomes:
|
||||
// Mime { Type: "text", Subtype: "html" }
|
||||
Type, Subtype string
|
||||
}
|
||||
|
||||
// M is shorthand for creating a MIME type.
|
||||
func M (ty, subtype string) Mime {
|
||||
return Mime { ty, subtype }
|
||||
}
|
||||
|
||||
// String returns the string representation of the MIME type.
|
||||
func (mime Mime) String () string {
|
||||
return mime.Type + "/" + mime.Subtype
|
||||
}
|
||||
|
||||
var MimePlain = Mime { "text", "plain" }
|
||||
|
||||
var MimeFile = Mime { "text", "uri-list" }
|
||||
|
||||
type byteReadCloser struct { *bytes.Reader }
|
||||
func (byteReadCloser) Close () error { return nil }
|
||||
|
||||
// Text returns plain text Data given a string.
|
||||
func Text (text string) Data {
|
||||
return Bytes(MimePlain, []byte(text))
|
||||
}
|
||||
|
||||
// Bytes constructs a Data given a buffer and a mime type.
|
||||
func Bytes (mime Mime, buffer []byte) Data {
|
||||
return Data {
|
||||
mime: byteReadCloser { bytes.NewReader(buffer) },
|
||||
}
|
||||
}
|
||||
|
||||
// Merge combines several Datas together. If multiple Datas provide a reader for
|
||||
// the same mime type, the ones further on in the list will take precedence.
|
||||
func Merge (individual ...Data) (combined Data) {
|
||||
for _, data := range individual {
|
||||
for mime, reader := range data {
|
||||
combined[mime] = reader
|
||||
}}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package config
|
||||
|
||||
import "time"
|
||||
import "tomo"
|
||||
|
||||
// Default specifies default configuration values.
|
||||
type Default struct { }
|
||||
|
||||
|
||||
// ScrollVelocity returns the default scroll velocity value.
|
||||
func (Default) ScrollVelocity () int {
|
||||
return 16
|
||||
}
|
||||
|
||||
// DoubleClickDelay returns the default double click delay.
|
||||
func (Default) DoubleClickDelay () time.Duration {
|
||||
return time.Second / 2
|
||||
}
|
||||
|
||||
// Wrapped wraps a configuration and uses Default if it is nil.
|
||||
type Wrapped struct {
|
||||
tomo.Config
|
||||
}
|
||||
|
||||
// ScrollVelocity returns how many pixels should be scrolled every time a scroll
|
||||
// button is pressed.
|
||||
func (wrapped Wrapped) ScrollVelocity () int {
|
||||
return wrapped.ensure().ScrollVelocity()
|
||||
}
|
||||
|
||||
// DoubleClickDelay returns the maximum delay between two clicks for them to be
|
||||
// registered as a double click.
|
||||
func (wrapped Wrapped) DoubleClickDelay () time.Duration {
|
||||
return wrapped.ensure().DoubleClickDelay()
|
||||
}
|
||||
|
||||
func (wrapped Wrapped) ensure () (real tomo.Config) {
|
||||
real = wrapped.Config
|
||||
if real == nil { real = Default { } }
|
||||
return
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// Package config implements a default configuration.
|
||||
package config
|
|
@ -0,0 +1,9 @@
|
|||
package config
|
||||
|
||||
// import "io"
|
||||
|
||||
// Parse parses one or more configuration files and returns them as a Config.
|
||||
// func Parse (sources ...io.Reader) (config tomo.Config) {
|
||||
// // TODO
|
||||
// return Default { }
|
||||
// }
|
Binary file not shown.
After Width: | Height: | Size: 350 B |
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
|
@ -0,0 +1,250 @@
|
|||
package theme
|
||||
|
||||
import "image"
|
||||
import "bytes"
|
||||
import _ "embed"
|
||||
import _ "image/png"
|
||||
import "image/color"
|
||||
import "golang.org/x/image/font"
|
||||
import "golang.org/x/image/font/basicfont"
|
||||
import "tomo"
|
||||
import "tomo/data"
|
||||
import "art"
|
||||
import "art/artutil"
|
||||
import "art/patterns"
|
||||
|
||||
//go:embed assets/default.png
|
||||
var defaultAtlasBytes []byte
|
||||
var defaultAtlas art.Canvas
|
||||
var defaultTextures [7][7]art.Pattern
|
||||
//go:embed assets/wintergreen-icons-small.png
|
||||
var defaultIconsSmallAtlasBytes []byte
|
||||
var defaultIconsSmall [640]binaryIcon
|
||||
//go:embed assets/wintergreen-icons-large.png
|
||||
var defaultIconsLargeAtlasBytes []byte
|
||||
var defaultIconsLarge [640]binaryIcon
|
||||
|
||||
func atlasCell (col, row int, border art.Inset) {
|
||||
bounds := image.Rect(0, 0, 8, 8).Add(image.Pt(col, row).Mul(8))
|
||||
defaultTextures[col][row] = patterns.Border {
|
||||
Canvas: art.Cut(defaultAtlas, bounds),
|
||||
Inset: border,
|
||||
}
|
||||
}
|
||||
|
||||
func atlasCol (col int, border art.Inset) {
|
||||
for index, _ := range defaultTextures[col] {
|
||||
atlasCell(col, index, border)
|
||||
}
|
||||
}
|
||||
|
||||
type binaryIcon struct {
|
||||
data []bool
|
||||
stride int
|
||||
}
|
||||
|
||||
func (icon binaryIcon) Draw (destination art.Canvas, color color.RGBA, at image.Point) {
|
||||
bounds := icon.Bounds().Add(at).Intersect(destination.Bounds())
|
||||
point := image.Point { }
|
||||
data, stride := destination.Buffer()
|
||||
|
||||
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
|
||||
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
|
||||
srcPoint := point.Sub(at)
|
||||
srcIndex := srcPoint.X + srcPoint.Y * icon.stride
|
||||
dstIndex := point.X + point.Y * stride
|
||||
if icon.data[srcIndex] {
|
||||
data[dstIndex] = color
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
func (icon binaryIcon) Bounds () image.Rectangle {
|
||||
return image.Rect(0, 0, icon.stride, len(icon.data) / icon.stride)
|
||||
}
|
||||
|
||||
func binaryIconFrom (source image.Image, clip image.Rectangle) (icon binaryIcon) {
|
||||
bounds := source.Bounds().Intersect(clip)
|
||||
if bounds.Empty() { return }
|
||||
|
||||
icon.stride = bounds.Dx()
|
||||
icon.data = make([]bool, bounds.Dx() * bounds.Dy())
|
||||
|
||||
point := image.Point { }
|
||||
dstIndex := 0
|
||||
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
|
||||
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
|
||||
r, g, b, a := source.At(point.X, point.Y).RGBA()
|
||||
if a > 0x8000 && (r + g + b) / 3 < 0x8000 {
|
||||
icon.data[dstIndex] = true
|
||||
}
|
||||
dstIndex ++
|
||||
}}
|
||||
return
|
||||
}
|
||||
|
||||
func init () {
|
||||
defaultAtlasImage, _, _ := image.Decode(bytes.NewReader(defaultAtlasBytes))
|
||||
defaultAtlas = art.FromImage(defaultAtlasImage)
|
||||
|
||||
atlasCol(0, art.I(0))
|
||||
atlasCol(1, art.I(3))
|
||||
atlasCol(2, art.I(1))
|
||||
atlasCol(3, art.I(1))
|
||||
atlasCol(4, art.I(1))
|
||||
atlasCol(5, art.I(3))
|
||||
atlasCol(6, art.I(1))
|
||||
|
||||
// set up small icons
|
||||
defaultIconsSmallAtlasImage, _, _ := image.Decode (
|
||||
bytes.NewReader(defaultIconsSmallAtlasBytes))
|
||||
point := image.Point { }
|
||||
iconIndex := 0
|
||||
for point.Y = 0; point.Y < 20; point.Y ++ {
|
||||
for point.X = 0; point.X < 32; point.X ++ {
|
||||
defaultIconsSmall[iconIndex] = binaryIconFrom (
|
||||
defaultIconsSmallAtlasImage,
|
||||
image.Rect(0, 0, 16, 16).Add(point.Mul(16)))
|
||||
iconIndex ++
|
||||
}}
|
||||
|
||||
// set up large icons
|
||||
defaultIconsLargeAtlasImage, _, _ := image.Decode (
|
||||
bytes.NewReader(defaultIconsLargeAtlasBytes))
|
||||
point = image.Point { }
|
||||
iconIndex = 0
|
||||
for point.Y = 0; point.Y < 8; point.Y ++ {
|
||||
for point.X = 0; point.X < 32; point.X ++ {
|
||||
defaultIconsLarge[iconIndex] = binaryIconFrom (
|
||||
defaultIconsLargeAtlasImage,
|
||||
image.Rect(0, 0, 32, 32).Add(point.Mul(32)))
|
||||
iconIndex ++
|
||||
}}
|
||||
iconIndex = 384
|
||||
for point.Y = 8; point.Y < 12; point.Y ++ {
|
||||
for point.X = 0; point.X < 32; point.X ++ {
|
||||
defaultIconsLarge[iconIndex] = binaryIconFrom (
|
||||
defaultIconsLargeAtlasImage,
|
||||
image.Rect(0, 0, 32, 32).Add(point.Mul(32)))
|
||||
iconIndex ++
|
||||
}}
|
||||
}
|
||||
|
||||
// Default is the default theme.
|
||||
type Default struct { }
|
||||
|
||||
// FontFace returns the default font face.
|
||||
func (Default) FontFace (style tomo.FontStyle, size tomo.FontSize, c tomo.Case) font.Face {
|
||||
return basicfont.Face7x13
|
||||
}
|
||||
|
||||
// Icon returns an icon from the default set corresponding to the given name.
|
||||
func (Default) Icon (id tomo.Icon, size tomo.IconSize, c tomo.Case) art.Icon {
|
||||
if size == tomo.IconSizeLarge {
|
||||
if id < 0 || int(id) >= len(defaultIconsLarge) {
|
||||
return nil
|
||||
} else {
|
||||
return defaultIconsLarge[id]
|
||||
}
|
||||
} else {
|
||||
if id < 0 || int(id) >= len(defaultIconsSmall) {
|
||||
return nil
|
||||
} else {
|
||||
return defaultIconsSmall[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MimeIcon returns an icon from the default set corresponding to the given mime.
|
||||
// type.
|
||||
func (Default) MimeIcon (data.Mime, tomo.IconSize, tomo.Case) art.Icon {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pattern returns a pattern from the default theme corresponding to the given
|
||||
// pattern ID.
|
||||
func (Default) Pattern (id tomo.Pattern, state tomo.State, c tomo.Case) art.Pattern {
|
||||
offset := 0; switch {
|
||||
case state.Disabled: offset = 1
|
||||
case state.Pressed && state.On: offset = 4
|
||||
case state.Focused && state.On: offset = 6
|
||||
case state.On: offset = 2
|
||||
case state.Pressed: offset = 3
|
||||
case state.Focused: offset = 5
|
||||
}
|
||||
|
||||
switch id {
|
||||
case tomo.PatternBackground: return patterns.Uhex(0xaaaaaaFF)
|
||||
case tomo.PatternDead: return defaultTextures[0][offset]
|
||||
case tomo.PatternRaised: return defaultTextures[1][offset]
|
||||
case tomo.PatternSunken: return defaultTextures[2][offset]
|
||||
case tomo.PatternPinboard: return defaultTextures[3][offset]
|
||||
case tomo.PatternButton: return defaultTextures[1][offset]
|
||||
case tomo.PatternInput: return defaultTextures[2][offset]
|
||||
case tomo.PatternGutter: return defaultTextures[2][offset]
|
||||
case tomo.PatternHandle: return defaultTextures[3][offset]
|
||||
case tomo.PatternLine: return defaultTextures[0][offset]
|
||||
case tomo.PatternMercury: return defaultTextures[4][offset]
|
||||
case tomo.PatternTableHead: return defaultTextures[5][offset]
|
||||
case tomo.PatternTableCell: return defaultTextures[5][offset]
|
||||
case tomo.PatternLamp: return defaultTextures[6][offset]
|
||||
default: return patterns.Uhex(0xFF00FFFF)
|
||||
}
|
||||
}
|
||||
|
||||
func (Default) Color (id tomo.Color, state tomo.State, c tomo.Case) color.RGBA {
|
||||
if state.Disabled { return artutil.Hex(0x444444FF) }
|
||||
|
||||
return artutil.Hex (map[tomo.Color] uint32 {
|
||||
tomo.ColorBlack: 0x272d24FF,
|
||||
tomo.ColorRed: 0x8c4230FF,
|
||||
tomo.ColorGreen: 0x69905fFF,
|
||||
tomo.ColorYellow: 0x9a973dFF,
|
||||
tomo.ColorBlue: 0x3d808fFF,
|
||||
tomo.ColorPurple: 0x8c608bFF,
|
||||
tomo.ColorCyan: 0x3d8f84FF,
|
||||
tomo.ColorWhite: 0xaea894FF,
|
||||
tomo.ColorBrightBlack: 0x4f5142FF,
|
||||
tomo.ColorBrightRed: 0xbd6f59FF,
|
||||
tomo.ColorBrightGreen: 0x8dad84FF,
|
||||
tomo.ColorBrightYellow: 0xe2c558FF,
|
||||
tomo.ColorBrightBlue: 0x77b1beFF,
|
||||
tomo.ColorBrightPurple: 0xc991c8FF,
|
||||
tomo.ColorBrightCyan: 0x74c7b7FF,
|
||||
tomo.ColorBrightWhite: 0xcfd7d2FF,
|
||||
|
||||
tomo.ColorForeground: 0x000000FF,
|
||||
tomo.ColorMidground: 0x656565FF,
|
||||
tomo.ColorBackground: 0xAAAAAAFF,
|
||||
tomo.ColorShadow: 0x000000FF,
|
||||
tomo.ColorShine: 0xFFFFFFFF,
|
||||
tomo.ColorAccent: 0xff3300FF,
|
||||
} [id])
|
||||
}
|
||||
|
||||
// Padding returns the default padding value for the given pattern.
|
||||
func (Default) Padding (id tomo.Pattern, c tomo.Case) art.Inset {
|
||||
switch id {
|
||||
case tomo.PatternGutter: return art.I(0)
|
||||
case tomo.PatternLine: return art.I(1)
|
||||
default: return art.I(6)
|
||||
}
|
||||
}
|
||||
|
||||
// Margin returns the default margin value for the given pattern.
|
||||
func (Default) Margin (id tomo.Pattern, c tomo.Case) image.Point {
|
||||
return image.Pt(6, 6)
|
||||
}
|
||||
|
||||
// Hints returns rendering optimization hints for a particular pattern.
|
||||
// These are optional, but following them may result in improved
|
||||
// performance.
|
||||
func (Default) Hints (pattern tomo.Pattern, c tomo.Case) (hints tomo.Hints) {
|
||||
return
|
||||
}
|
||||
|
||||
// Sink returns the default sink vector for the given pattern.
|
||||
func (Default) Sink (pattern tomo.Pattern, c tomo.Case) image.Point {
|
||||
return image.Point { 1, 1 }
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// Package theme implements a default theme.
|
||||
package theme
|
|
@ -1,47 +0,0 @@
|
|||
package defaultfont
|
||||
|
||||
import "golang.org/x/image/font/basicfont"
|
||||
|
||||
var FaceRegular = basicfont.Face7x13
|
||||
|
||||
// FIXME: make bold, italic, and bold italic masks by processing the Face7x13
|
||||
// mask.
|
||||
|
||||
var FaceBold = &basicfont.Face {
|
||||
Advance: 7,
|
||||
Width: 6,
|
||||
Height: 13,
|
||||
Ascent: 11,
|
||||
Descent: 2,
|
||||
Mask: FaceRegular.Mask, // TODO
|
||||
Ranges: []basicfont.Range {
|
||||
{ '\u0020', '\u007f', 0 },
|
||||
{ '\ufffd', '\ufffe', 95 },
|
||||
},
|
||||
}
|
||||
|
||||
var FaceItalic = &basicfont.Face {
|
||||
Advance: 7,
|
||||
Width: 6,
|
||||
Height: 13,
|
||||
Ascent: 11,
|
||||
Descent: 2,
|
||||
Mask: FaceRegular.Mask, // TODO
|
||||
Ranges: []basicfont.Range {
|
||||
{ '\u0020', '\u007f', 0 },
|
||||
{ '\ufffd', '\ufffe', 95 },
|
||||
},
|
||||
}
|
||||
|
||||
var FaceBoldItalic = &basicfont.Face {
|
||||
Advance: 7,
|
||||
Width: 6,
|
||||
Height: 13,
|
||||
Ascent: 11,
|
||||
Descent: 2,
|
||||
Mask: FaceRegular.Mask, // TODO
|
||||
Ranges: []basicfont.Range {
|
||||
{ '\u0020', '\u007f', 0 },
|
||||
{ '\ufffd', '\ufffe', 95 },
|
||||
},
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
// Package dirs provides access to standard system and user directories.
|
||||
package dirs
|
||||
|
||||
import "os"
|
||||
import "strings"
|
||||
import "path/filepath"
|
||||
|
||||
var homeDirectory string
|
||||
var configHome string
|
||||
var configDirs []string
|
||||
var dataHome string
|
||||
var dataDirs []string
|
||||
var cacheHome string
|
||||
|
||||
func init () {
|
||||
var err error
|
||||
homeDirectory, err = os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic("could not get user home directory: " + err.Error())
|
||||
}
|
||||
|
||||
configHome = os.Getenv("XDG_CONFIG_HOME")
|
||||
if configHome == "" {
|
||||
configHome = filepath.Join(homeDirectory, "/.config/")
|
||||
}
|
||||
|
||||
configDirsString := os.Getenv("XDG_CONFIG_DIRS")
|
||||
if configDirsString == "" {
|
||||
configDirsString = "/etc/xdg/"
|
||||
}
|
||||
configDirs = append(strings.Split(configDirsString, ":"), configHome)
|
||||
|
||||
dataHome = os.Getenv("XDG_DATA_HOME")
|
||||
if dataHome == "" {
|
||||
dataHome = filepath.Join(homeDirectory, "/.local/share/")
|
||||
}
|
||||
|
||||
dataDirsString := os.Getenv("XDG_CONFIG_DIRS")
|
||||
if dataDirsString == "" {
|
||||
dataDirsString = "/usr/local/share/:/usr/share/"
|
||||
}
|
||||
configDirs = append(strings.Split(configDirsString, ":"), configHome)
|
||||
|
||||
cacheHome = os.Getenv("XDG_CACHE_HOME")
|
||||
if cacheHome == "" {
|
||||
cacheHome = filepath.Join(homeDirectory, "/.cache/")
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigHome returns the path to the directory where user configuration files
|
||||
// should be stored.
|
||||
func ConfigHome (name string) (home string) {
|
||||
return filepath.Join(configHome, name)
|
||||
}
|
||||
|
||||
// ConfigDirs returns all paths where configuration files might exist.
|
||||
func ConfigDirs (name string) (dirs []string) {
|
||||
dirs = make([]string, len(configDirs))
|
||||
for index, dir := range configDirs {
|
||||
dirs[index] = filepath.Join(dir, name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DataHome returns the path to the directory where user data should be stored.
|
||||
func DataHome (name string) (home string) {
|
||||
return filepath.Join(dataHome, name)
|
||||
}
|
||||
|
||||
// DataDirs returns all paths where data files might exist.
|
||||
func DataDirs (name string) (dirs []string) {
|
||||
dirs = make([]string, len(dataDirs))
|
||||
for index, dir := range dataDirs {
|
||||
dirs[index] = filepath.Join(dir, name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CacheHome returns the path to the directory where user cache files should be
|
||||
// stored.
|
||||
func CacheHome (name string) (home string) {
|
||||
return filepath.Join(cacheHome, name)
|
||||
}
|
180
element.go
180
element.go
|
@ -1,177 +1,15 @@
|
|||
package tomo
|
||||
|
||||
import "image"
|
||||
import "art"
|
||||
|
||||
// Element represents a basic on-screen object.
|
||||
// Element represents a basic on-screen object. Extended element interfaces are
|
||||
// defined in the ability module.
|
||||
type Element interface {
|
||||
// Element must implement the Canvas interface. Elements should start
|
||||
// out with a completely blank buffer, and only allocate memory and draw
|
||||
// on it for the first time when sent an EventResize event.
|
||||
Canvas
|
||||
// Draw causes the element to draw to the specified canvas. The bounds
|
||||
// of this canvas specify the area that is actually drawn to, while the
|
||||
// Entity bounds specify the actual area of the element.
|
||||
Draw (art.Canvas)
|
||||
|
||||
// MinimumSize specifies the minimum amount of pixels this element's
|
||||
// width and height may be set to. If the element is given a resize
|
||||
// event with dimensions smaller than this, it will use its minimum
|
||||
// instead of the offending dimension(s).
|
||||
MinimumSize () (width, height int)
|
||||
|
||||
// DrawTo sets this element's canvas. This should only be called by the
|
||||
// parent element. This is typically a region of the parent element's
|
||||
// canvas.
|
||||
DrawTo (canvas Canvas)
|
||||
|
||||
// OnDamage sets a function to be called when an area of the element is
|
||||
// drawn on and should be pushed to the screen.
|
||||
OnDamage (callback func (region Canvas))
|
||||
|
||||
// OnMinimumSizeChange sets a function to be called when the element's
|
||||
// minimum size is changed.
|
||||
OnMinimumSizeChange (callback func ())
|
||||
}
|
||||
|
||||
// KeynavDirection represents a keyboard navigation direction.
|
||||
type KeynavDirection int
|
||||
|
||||
const (
|
||||
KeynavDirectionNeutral KeynavDirection = 0
|
||||
KeynavDirectionBackward KeynavDirection = -1
|
||||
KeynavDirectionForward KeynavDirection = 1
|
||||
)
|
||||
|
||||
// Canon returns a well-formed direction.
|
||||
func (direction KeynavDirection) Canon () (canon KeynavDirection) {
|
||||
if direction > 0 {
|
||||
return KeynavDirectionForward
|
||||
} else if direction == 0 {
|
||||
return KeynavDirectionNeutral
|
||||
} else {
|
||||
return KeynavDirectionBackward
|
||||
}
|
||||
}
|
||||
|
||||
// Focusable represents an element that has keyboard navigation support. This
|
||||
// includes inputs, buttons, sliders, etc. as well as any elements that have
|
||||
// children (so keyboard navigation events can be propagated downward).
|
||||
type Focusable interface {
|
||||
Element
|
||||
|
||||
// Focused returns whether or not this element is currently focused.
|
||||
Focused () (selected bool)
|
||||
|
||||
// Focus focuses this element, if its parent element grants the
|
||||
// request.
|
||||
Focus ()
|
||||
|
||||
// HandleFocus causes this element to mark itself as focused. If the
|
||||
// element does not have children, it is disabled, or there are no more
|
||||
// selectable children in the given direction, it should return false
|
||||
// and do nothing. Otherwise, it should select itself and any children
|
||||
// (if applicable) and return true.
|
||||
HandleFocus (direction KeynavDirection) (accepted bool)
|
||||
|
||||
// HandleDeselection causes this element to mark itself and all of its
|
||||
// children as unfocused.
|
||||
HandleUnfocus ()
|
||||
|
||||
// OnFocusRequest sets a function to be called when this element wants
|
||||
// its parent element to focus it. Parent elements should return true if
|
||||
// the request was granted, and false if it was not.
|
||||
OnFocusRequest (func () (granted bool))
|
||||
|
||||
// OnFocusMotionRequest sets a function to be called when this
|
||||
// element wants its parent element to focus the element behind or in
|
||||
// front of it, depending on the specified direction. Parent elements
|
||||
// should return true if the request was granted, and false if it was
|
||||
// not.
|
||||
OnFocusMotionRequest (func (direction KeynavDirection) (granted bool))
|
||||
}
|
||||
|
||||
// KeyboardTarget represents an element that can receive keyboard input.
|
||||
type KeyboardTarget interface {
|
||||
Element
|
||||
|
||||
// HandleKeyDown is called when a key is pressed down or repeated while
|
||||
// this element has keyboard focus. It is important to note that not
|
||||
// every key down event is guaranteed to be paired with exactly one key
|
||||
// up event. This is the reason a list of modifier keys held down at the
|
||||
// time of the key press is given.
|
||||
HandleKeyDown (key Key, modifiers Modifiers)
|
||||
|
||||
// HandleKeyUp is called when a key is released while this element has
|
||||
// keyboard focus.
|
||||
HandleKeyUp (key Key, modifiers Modifiers)
|
||||
}
|
||||
|
||||
// MouseTarget represents an element that can receive mouse events.
|
||||
type MouseTarget interface {
|
||||
Element
|
||||
|
||||
// Each of these handler methods is passed the position of the mouse
|
||||
// cursor at the time of the event as x, y.
|
||||
|
||||
// HandleMouseDown is called when a mouse button is pressed down on this
|
||||
// element.
|
||||
HandleMouseDown (x, y int, button Button)
|
||||
|
||||
// HandleMouseUp is called when a mouse button is released that was
|
||||
// originally pressed down on this element.
|
||||
HandleMouseUp (x, y int, button Button)
|
||||
|
||||
// HandleMouseMove is called when the mouse is moved over this element,
|
||||
// or the mouse is moving while being held down and originally pressed
|
||||
// down on this element.
|
||||
HandleMouseMove (x, y int)
|
||||
|
||||
// HandleScroll is called when the mouse is scrolled. The X and Y
|
||||
// direction of the scroll event are passed as deltaX and deltaY.
|
||||
HandleMouseScroll (x, y int, deltaX, deltaY float64)
|
||||
}
|
||||
|
||||
// Flexible represents an element who's preferred minimum height can change in
|
||||
// response to its width.
|
||||
type Flexible interface {
|
||||
Element
|
||||
|
||||
// FlexibleHeightFor returns what the element's minimum height would be
|
||||
// if resized to a specified width. This does not actually alter the
|
||||
// state of the element in any way, but it may perform significant work,
|
||||
// so it should be called sparingly.
|
||||
//
|
||||
// It is reccomended that parent containers check for this interface and
|
||||
// take this method's value into account in order to support things like
|
||||
// flow layouts and text wrapping, but it is not absolutely necessary.
|
||||
// The element's MinimumSize method will still return the absolute
|
||||
// minimum size that the element may be resized to.
|
||||
//
|
||||
// It is important to note that if a parent container checks for
|
||||
// flexible chilren, it itself will likely need to be flexible.
|
||||
FlexibleHeightFor (width int) (height int)
|
||||
|
||||
// OnFlexibleHeightChange sets a function to be called when the
|
||||
// parameters affecting this element's flexible height are changed.
|
||||
OnFlexibleHeightChange (callback func ())
|
||||
}
|
||||
|
||||
// Scrollable represents an element that can be scrolled. It acts as a viewport
|
||||
// through which its contents can be observed.
|
||||
type Scrollable interface {
|
||||
Element
|
||||
|
||||
// ScrollContentBounds returns the full content size of the element.
|
||||
ScrollContentBounds () (bounds image.Rectangle)
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's
|
||||
// viewport relative to ScrollBounds.
|
||||
ScrollViewportBounds () (bounds image.Rectangle)
|
||||
|
||||
// ScrollTo scrolls the viewport to the specified point relative to
|
||||
// ScrollBounds.
|
||||
ScrollTo (position image.Point)
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
ScrollAxes () (horizontal, vertical bool)
|
||||
|
||||
// OnScrollBoundsChange sets a function to be called when the element's
|
||||
// ScrollContentBounds, ScrollViewportBounds, or ScrollAxes are changed.
|
||||
OnScrollBoundsChange (callback func ())
|
||||
// Entity returns this element's entity.
|
||||
Entity () Entity
|
||||
}
|
||||
|
|
|
@ -1,153 +0,0 @@
|
|||
package basic
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
var buttonCase = theme.C("basic", "button")
|
||||
|
||||
// Button is a clickable button.
|
||||
type Button struct {
|
||||
*core.Core
|
||||
*core.FocusableCore
|
||||
core core.CoreControl
|
||||
focusableControl core.FocusableCoreControl
|
||||
drawer artist.TextDrawer
|
||||
|
||||
pressed bool
|
||||
text string
|
||||
|
||||
onClick func ()
|
||||
}
|
||||
|
||||
// NewButton creates a new button with the specified label text.
|
||||
func NewButton (text string) (element *Button) {
|
||||
element = &Button { }
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
element.FocusableCore,
|
||||
element.focusableControl = core.NewFocusableCore (func () {
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
})
|
||||
element.drawer.SetFace(theme.FontFaceRegular())
|
||||
element.SetText(text)
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Button) HandleMouseDown (x, y int, button tomo.Button) {
|
||||
if !element.Enabled() { return }
|
||||
if !element.Focused() { element.Focus() }
|
||||
if button != tomo.ButtonLeft { return }
|
||||
element.pressed = true
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Button) HandleMouseUp (x, y int, button tomo.Button) {
|
||||
if button != tomo.ButtonLeft { return }
|
||||
element.pressed = false
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
|
||||
within := image.Point { x, y }.
|
||||
In(element.Bounds())
|
||||
|
||||
if !element.Enabled() { return }
|
||||
if within && element.onClick != nil {
|
||||
element.onClick()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Button) HandleMouseMove (x, y int) { }
|
||||
func (element *Button) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||
|
||||
func (element *Button) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
if key == tomo.KeyEnter {
|
||||
element.pressed = true
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Button) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) {
|
||||
if key == tomo.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if !element.Enabled() { return }
|
||||
if element.onClick != nil {
|
||||
element.onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnClick sets the function to be called when the button is clicked.
|
||||
func (element *Button) OnClick (callback func ()) {
|
||||
element.onClick = callback
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this button can be clicked or not.
|
||||
func (element *Button) SetEnabled (enabled bool) {
|
||||
element.focusableControl.SetEnabled(enabled)
|
||||
}
|
||||
|
||||
// SetText sets the button's label text.
|
||||
func (element *Button) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
_, inset := theme.ButtonPattern(theme.PatternState { Case: buttonCase })
|
||||
minimumSize := inset.Inverse().Apply(textBounds).Inset(-theme.Padding())
|
||||
element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Button) draw () {
|
||||
bounds := element.Bounds()
|
||||
|
||||
pattern, inset := theme.ButtonPattern(theme.PatternState {
|
||||
Case: buttonCase,
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.Focused(),
|
||||
Pressed: element.pressed,
|
||||
})
|
||||
|
||||
artist.FillRectangle(element, pattern, bounds)
|
||||
|
||||
innerBounds := inset.Apply(bounds)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
offset := image.Point {
|
||||
X: innerBounds.Min.X + (innerBounds.Dx() - textBounds.Dx()) / 2,
|
||||
Y: innerBounds.Min.Y + (innerBounds.Dy() - textBounds.Dy()) / 2,
|
||||
}
|
||||
|
||||
// account for the fact that the bounding rectangle will be shifted over
|
||||
// due to the bounds origin being at the baseline of the first line
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
|
||||
foreground, _ := theme.ForegroundPattern (theme.PatternState {
|
||||
Case: buttonCase,
|
||||
Disabled: !element.Enabled(),
|
||||
})
|
||||
element.drawer.Draw(element, foreground, offset)
|
||||
}
|
|
@ -1,170 +0,0 @@
|
|||
package basic
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
var checkboxCase = theme.C("basic", "checkbox")
|
||||
|
||||
// Checkbox is a toggle-able checkbox with a label.
|
||||
type Checkbox struct {
|
||||
*core.Core
|
||||
*core.FocusableCore
|
||||
core core.CoreControl
|
||||
focusableControl core.FocusableCoreControl
|
||||
drawer artist.TextDrawer
|
||||
|
||||
pressed bool
|
||||
checked bool
|
||||
text string
|
||||
|
||||
onToggle func ()
|
||||
}
|
||||
|
||||
// NewCheckbox creates a new cbeckbox with the specified label text.
|
||||
func NewCheckbox (text string, checked bool) (element *Checkbox) {
|
||||
element = &Checkbox { checked: checked }
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
element.FocusableCore,
|
||||
element.focusableControl = core.NewFocusableCore (func () {
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
})
|
||||
element.drawer.SetFace(theme.FontFaceRegular())
|
||||
element.SetText(text)
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleMouseDown (x, y int, button tomo.Button) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
element.pressed = true
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleMouseUp (x, y int, button tomo.Button) {
|
||||
if button != tomo.ButtonLeft || !element.pressed { return }
|
||||
|
||||
element.pressed = false
|
||||
within := image.Point { x, y }.
|
||||
In(element.Bounds())
|
||||
if within {
|
||||
element.checked = !element.checked
|
||||
}
|
||||
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if within && element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleMouseMove (x, y int) { }
|
||||
func (element *Checkbox) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||
|
||||
func (element *Checkbox) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) {
|
||||
if key == tomo.KeyEnter {
|
||||
element.pressed = true
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) {
|
||||
if key == tomo.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
element.checked = !element.checked
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnToggle sets the function to be called when the checkbox is toggled.
|
||||
func (element *Checkbox) OnToggle (callback func ()) {
|
||||
element.onToggle = callback
|
||||
}
|
||||
|
||||
// Value reports whether or not the checkbox is currently checked.
|
||||
func (element *Checkbox) Value () (checked bool) {
|
||||
return element.checked
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this checkbox can be toggled or not.
|
||||
func (element *Checkbox) SetEnabled (enabled bool) {
|
||||
element.focusableControl.SetEnabled(enabled)
|
||||
}
|
||||
|
||||
// SetText sets the checkbox's label text.
|
||||
func (element *Checkbox) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
|
||||
if text == "" {
|
||||
element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy())
|
||||
} else {
|
||||
element.core.SetMinimumSize (
|
||||
textBounds.Dy() + theme.Padding() + textBounds.Dx(),
|
||||
textBounds.Dy())
|
||||
}
|
||||
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Checkbox) draw () {
|
||||
bounds := element.Bounds()
|
||||
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
|
||||
|
||||
backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState {
|
||||
Case: checkboxCase,
|
||||
})
|
||||
artist.FillRectangle(element, backgroundPattern, bounds)
|
||||
|
||||
pattern, inset := theme.ButtonPattern(theme.PatternState {
|
||||
Case: checkboxCase,
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.Focused(),
|
||||
Pressed: element.pressed,
|
||||
})
|
||||
artist.FillRectangle(element, pattern, boxBounds)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
offset := bounds.Min.Add(image.Point {
|
||||
X: bounds.Dy() + theme.Padding(),
|
||||
})
|
||||
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
|
||||
foreground, _ := theme.ForegroundPattern (theme.PatternState {
|
||||
Case: checkboxCase,
|
||||
Disabled: !element.Enabled(),
|
||||
})
|
||||
element.drawer.Draw(element, foreground, offset)
|
||||
|
||||
if element.checked {
|
||||
checkBounds := inset.Apply(boxBounds).Inset(2)
|
||||
artist.FillRectangle(element, foreground, checkBounds)
|
||||
}
|
||||
}
|
|
@ -1,475 +0,0 @@
|
|||
package basic
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
var containerCase = theme.C("basic", "container")
|
||||
|
||||
// Container is an element capable of containg other elements, and arranging
|
||||
// them in a layout.
|
||||
type Container struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
|
||||
layout tomo.Layout
|
||||
children []tomo.LayoutEntry
|
||||
drags [10]tomo.MouseTarget
|
||||
warping bool
|
||||
focused bool
|
||||
focusable bool
|
||||
flexible bool
|
||||
|
||||
onFocusRequest func () (granted bool)
|
||||
onFocusMotionRequest func (tomo.KeynavDirection) (granted bool)
|
||||
onFlexibleHeightChange func ()
|
||||
}
|
||||
|
||||
// NewContainer creates a new container.
|
||||
func NewContainer (layout tomo.Layout) (element *Container) {
|
||||
element = &Container { }
|
||||
element.Core, element.core = core.NewCore(element.redoAll)
|
||||
element.SetLayout(layout)
|
||||
return
|
||||
}
|
||||
|
||||
// SetLayout sets the layout of this container.
|
||||
func (element *Container) SetLayout (layout tomo.Layout) {
|
||||
element.layout = layout
|
||||
if element.core.HasImage() {
|
||||
element.redoAll()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
// Adopt adds a new child element to the container. If expand is set to true,
|
||||
// the element will expand (instead of contract to its minimum size), in
|
||||
// whatever way is defined by the current layout.
|
||||
func (element *Container) Adopt (child tomo.Element, expand bool) {
|
||||
// set event handlers
|
||||
child.OnDamage (func (region tomo.Canvas) {
|
||||
element.core.DamageRegion(region.Bounds())
|
||||
})
|
||||
child.OnMinimumSizeChange(element.updateMinimumSize)
|
||||
if child0, ok := child.(tomo.Flexible); ok {
|
||||
child0.OnFlexibleHeightChange(element.updateMinimumSize)
|
||||
}
|
||||
if child0, ok := child.(tomo.Focusable); ok {
|
||||
child0.OnFocusRequest (func () (granted bool) {
|
||||
return element.childFocusRequestCallback(child0)
|
||||
})
|
||||
child0.OnFocusMotionRequest (
|
||||
func (direction tomo.KeynavDirection) (granted bool) {
|
||||
if element.onFocusMotionRequest == nil { return }
|
||||
return element.onFocusMotionRequest(direction)
|
||||
})
|
||||
}
|
||||
|
||||
// add child
|
||||
element.children = append (element.children, tomo.LayoutEntry {
|
||||
Element: child,
|
||||
Expand: expand,
|
||||
})
|
||||
|
||||
// refresh stale data
|
||||
element.updateMinimumSize()
|
||||
element.reflectChildProperties()
|
||||
if element.core.HasImage() && !element.warping {
|
||||
element.redoAll()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
// Warp runs the specified callback, deferring all layout and rendering updates
|
||||
// until the callback has finished executing. This allows for aplications to
|
||||
// perform batch gui updates without flickering and stuff.
|
||||
func (element *Container) Warp (callback func ()) {
|
||||
if element.warping {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
element.warping = true
|
||||
callback()
|
||||
element.warping = false
|
||||
|
||||
// TODO: create some sort of task list so we don't do a full recalculate
|
||||
// and redraw every time, because although that is the most likely use
|
||||
// case, it is not the only one.
|
||||
if element.core.HasImage() {
|
||||
element.redoAll()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
// Disown removes the given child from the container if it is contained within
|
||||
// it.
|
||||
func (element *Container) Disown (child tomo.Element) {
|
||||
for index, entry := range element.children {
|
||||
if entry.Element == child {
|
||||
element.clearChildEventHandlers(entry.Element)
|
||||
element.children = append (
|
||||
element.children[:index],
|
||||
element.children[index + 1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
element.updateMinimumSize()
|
||||
element.reflectChildProperties()
|
||||
if element.core.HasImage() && !element.warping {
|
||||
element.redoAll()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) clearChildEventHandlers (child tomo.Element) {
|
||||
child.DrawTo(nil)
|
||||
child.OnDamage(nil)
|
||||
child.OnMinimumSizeChange(nil)
|
||||
if child0, ok := child.(tomo.Focusable); ok {
|
||||
child0.OnFocusRequest(nil)
|
||||
child0.OnFocusMotionRequest(nil)
|
||||
if child0.Focused() {
|
||||
child0.HandleUnfocus()
|
||||
}
|
||||
}
|
||||
if child0, ok := child.(tomo.Flexible); ok {
|
||||
child0.OnFlexibleHeightChange(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// DisownAll removes all child elements from the container at once.
|
||||
func (element *Container) DisownAll () {
|
||||
element.children = nil
|
||||
|
||||
element.updateMinimumSize()
|
||||
element.reflectChildProperties()
|
||||
if element.core.HasImage() && !element.warping {
|
||||
element.redoAll()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
// Children returns a slice containing this element's children.
|
||||
func (element *Container) Children () (children []tomo.Element) {
|
||||
children = make([]tomo.Element, len(element.children))
|
||||
for index, entry := range element.children {
|
||||
children[index] = entry.Element
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CountChildren returns the amount of children contained within this element.
|
||||
func (element *Container) CountChildren () (count int) {
|
||||
return len(element.children)
|
||||
}
|
||||
|
||||
// Child returns the child at the specified index. If the index is out of
|
||||
// bounds, this method will return nil.
|
||||
func (element *Container) Child (index int) (child tomo.Element) {
|
||||
if index < 0 || index > len(element.children) { return }
|
||||
return element.children[index].Element
|
||||
}
|
||||
|
||||
// ChildAt returns the child that contains the specified x and y coordinates. If
|
||||
// there are no children at the coordinates, this method will return nil.
|
||||
func (element *Container) ChildAt (point image.Point) (child tomo.Element) {
|
||||
for _, entry := range element.children {
|
||||
if point.In(entry.Bounds) {
|
||||
child = entry.Element
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Container) childPosition (child tomo.Element) (position image.Point) {
|
||||
for _, entry := range element.children {
|
||||
if entry.Element == child {
|
||||
position = entry.Bounds.Min
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Container) redoAll () {
|
||||
// do a layout
|
||||
element.recalculate()
|
||||
|
||||
// draw a background
|
||||
bounds := element.Bounds()
|
||||
pattern, _ := theme.BackgroundPattern (theme.PatternState {
|
||||
Case: containerCase,
|
||||
})
|
||||
artist.FillRectangle(element, pattern, bounds)
|
||||
|
||||
// cut our canvas up and give peices to child elements
|
||||
for _, entry := range element.children {
|
||||
entry.DrawTo(tomo.Cut(element, entry.Bounds))
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) HandleMouseDown (x, y int, button tomo.Button) {
|
||||
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(tomo.MouseTarget)
|
||||
if !handlesMouse { return }
|
||||
element.drags[button] = child
|
||||
child.HandleMouseDown(x, y, button)
|
||||
}
|
||||
|
||||
func (element *Container) HandleMouseUp (x, y int, button tomo.Button) {
|
||||
child := element.drags[button]
|
||||
if child == nil { return }
|
||||
element.drags[button] = nil
|
||||
child.HandleMouseUp(x, y, button)
|
||||
}
|
||||
|
||||
func (element *Container) HandleMouseMove (x, y int) {
|
||||
for _, child := range element.drags {
|
||||
if child == nil { continue }
|
||||
child.HandleMouseMove(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) HandleMouseScroll (x, y int, deltaX, deltaY float64) {
|
||||
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(tomo.MouseTarget)
|
||||
if !handlesMouse { return }
|
||||
child.HandleMouseScroll(x, y, deltaX, deltaY)
|
||||
}
|
||||
|
||||
func (element *Container) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) {
|
||||
element.forFocused (func (child tomo.Focusable) bool {
|
||||
child0, handlesKeyboard := child.(tomo.KeyboardTarget)
|
||||
if handlesKeyboard {
|
||||
child0.HandleKeyDown(key, modifiers)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (element *Container) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) {
|
||||
element.forFocused (func (child tomo.Focusable) bool {
|
||||
child0, handlesKeyboard := child.(tomo.KeyboardTarget)
|
||||
if handlesKeyboard {
|
||||
child0.HandleKeyUp(key, modifiers)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (element *Container) FlexibleHeightFor (width int) (height int) {
|
||||
return element.layout.FlexibleHeightFor(element.children, width)
|
||||
}
|
||||
|
||||
func (element *Container) OnFlexibleHeightChange (callback func ()) {
|
||||
element.onFlexibleHeightChange = callback
|
||||
}
|
||||
|
||||
func (element *Container) Focused () (focused bool) {
|
||||
return element.focused
|
||||
}
|
||||
|
||||
func (element *Container) Focus () {
|
||||
if element.onFocusRequest != nil {
|
||||
element.onFocusRequest()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) HandleFocus (direction tomo.KeynavDirection) (ok bool) {
|
||||
if !element.focusable { return false }
|
||||
direction = direction.Canon()
|
||||
|
||||
firstFocused := element.firstFocused()
|
||||
if firstFocused < 0 {
|
||||
// no element is currently focused, so we need to focus either
|
||||
// the first or last focusable element depending on the
|
||||
// direction.
|
||||
switch direction {
|
||||
case tomo.KeynavDirectionNeutral, tomo.KeynavDirectionForward:
|
||||
// if we recieve a neutral or forward direction, focus
|
||||
// the first focusable element.
|
||||
return element.focusFirstFocusableElement(direction)
|
||||
|
||||
case tomo.KeynavDirectionBackward:
|
||||
// if we recieve a backward direction, focus the last
|
||||
// focusable element.
|
||||
return element.focusLastFocusableElement(direction)
|
||||
}
|
||||
} else {
|
||||
// an element is currently focused, so we need to move the
|
||||
// focus in the specified direction
|
||||
firstFocusedChild :=
|
||||
element.children[firstFocused].Element.(tomo.Focusable)
|
||||
|
||||
// before we move the focus, the currently focused child
|
||||
// may also be able to move its focus. if the child is able
|
||||
// to do that, we will let it and not move ours.
|
||||
if firstFocusedChild.HandleFocus(direction) {
|
||||
return true
|
||||
}
|
||||
|
||||
// find the previous/next focusable element relative to the
|
||||
// currently focused element, if it exists.
|
||||
for index := firstFocused + int(direction);
|
||||
index < len(element.children) && index >= 0;
|
||||
index += int(direction) {
|
||||
|
||||
child, focusable :=
|
||||
element.children[index].
|
||||
Element.(tomo.Focusable)
|
||||
if focusable && child.HandleFocus(direction) {
|
||||
// we have found one, so we now actually move
|
||||
// the focus.
|
||||
firstFocusedChild.HandleUnfocus()
|
||||
element.focused = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (element *Container) focusFirstFocusableElement (
|
||||
direction tomo.KeynavDirection,
|
||||
) (
|
||||
ok bool,
|
||||
) {
|
||||
element.forFocusable (func (child tomo.Focusable) bool {
|
||||
if child.HandleFocus(direction) {
|
||||
element.focused = true
|
||||
ok = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Container) focusLastFocusableElement (
|
||||
direction tomo.KeynavDirection,
|
||||
) (
|
||||
ok bool,
|
||||
) {
|
||||
element.forFocusableBackward (func (child tomo.Focusable) bool {
|
||||
if child.HandleFocus(direction) {
|
||||
element.focused = true
|
||||
ok = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Container) HandleUnfocus () {
|
||||
element.focused = false
|
||||
element.forFocused (func (child tomo.Focusable) bool {
|
||||
child.HandleUnfocus()
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (element *Container) OnFocusRequest (callback func () (granted bool)) {
|
||||
element.onFocusRequest = callback
|
||||
}
|
||||
|
||||
func (element *Container) OnFocusMotionRequest (
|
||||
callback func (direction tomo.KeynavDirection) (granted bool),
|
||||
) {
|
||||
element.onFocusMotionRequest = callback
|
||||
}
|
||||
|
||||
func (element *Container) forFocused (callback func (child tomo.Focusable) bool) {
|
||||
for _, entry := range element.children {
|
||||
child, focusable := entry.Element.(tomo.Focusable)
|
||||
if focusable && child.Focused() {
|
||||
if !callback(child) { break }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) forFocusable (callback func (child tomo.Focusable) bool) {
|
||||
for _, entry := range element.children {
|
||||
child, focusable := entry.Element.(tomo.Focusable)
|
||||
if focusable {
|
||||
if !callback(child) { break }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) forFlexible (callback func (child tomo.Flexible) bool) {
|
||||
for _, entry := range element.children {
|
||||
child, flexible := entry.Element.(tomo.Flexible)
|
||||
if flexible {
|
||||
if !callback(child) { break }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) forFocusableBackward (callback func (child tomo.Focusable) bool) {
|
||||
for index := len(element.children) - 1; index >= 0; index -- {
|
||||
child, focusable := element.children[index].Element.(tomo.Focusable)
|
||||
if focusable {
|
||||
if !callback(child) { break }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) firstFocused () (index int) {
|
||||
for currentIndex, entry := range element.children {
|
||||
child, focusable := entry.Element.(tomo.Focusable)
|
||||
if focusable && child.Focused() {
|
||||
return currentIndex
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (element *Container) reflectChildProperties () {
|
||||
element.focusable = false
|
||||
element.forFocusable (func (tomo.Focusable) bool {
|
||||
element.focusable = true
|
||||
return false
|
||||
})
|
||||
element.flexible = false
|
||||
element.forFlexible (func (tomo.Flexible) bool {
|
||||
element.flexible = true
|
||||
return false
|
||||
})
|
||||
if !element.focusable {
|
||||
element.focused = false
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) childFocusRequestCallback (
|
||||
child tomo.Focusable,
|
||||
) (
|
||||
granted bool,
|
||||
) {
|
||||
if element.onFocusRequest != nil && element.onFocusRequest() {
|
||||
element.forFocused (func (child tomo.Focusable) bool {
|
||||
child.HandleUnfocus()
|
||||
return true
|
||||
})
|
||||
child.HandleFocus(tomo.KeynavDirectionNeutral)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) updateMinimumSize () {
|
||||
width, height := element.layout.MinimumSize(element.children)
|
||||
if element.flexible {
|
||||
height = element.layout.FlexibleHeightFor(element.children, width)
|
||||
}
|
||||
element.core.SetMinimumSize(width, height)
|
||||
}
|
||||
|
||||
func (element *Container) recalculate () {
|
||||
element.layout.Arrange(element.children, element.Bounds())
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
package basic
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
var labelCase = theme.C("basic", "label")
|
||||
|
||||
// Label is a simple text box.
|
||||
type Label struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
|
||||
wrap bool
|
||||
text string
|
||||
drawer artist.TextDrawer
|
||||
|
||||
onFlexibleHeightChange func ()
|
||||
}
|
||||
|
||||
// NewLabel creates a new label. If wrap is set to true, the text inside will be
|
||||
// wrapped.
|
||||
func NewLabel (text string, wrap bool) (element *Label) {
|
||||
element = &Label { }
|
||||
element.Core, element.core = core.NewCore(element.handleResize)
|
||||
face := theme.FontFaceRegular()
|
||||
element.drawer.SetFace(face)
|
||||
element.SetWrap(wrap)
|
||||
element.SetText(text)
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Label) handleResize () {
|
||||
bounds := element.Bounds()
|
||||
if element.wrap {
|
||||
element.drawer.SetMaxWidth(bounds.Dx())
|
||||
element.drawer.SetMaxHeight(bounds.Dy())
|
||||
}
|
||||
element.draw()
|
||||
return
|
||||
}
|
||||
|
||||
// FlexibleHeightFor returns the reccomended height for this element based on
|
||||
// the given width in order to allow the text to wrap properly.
|
||||
func (element *Label) FlexibleHeightFor (width int) (height int) {
|
||||
if element.wrap {
|
||||
return element.drawer.ReccomendedHeightFor(width)
|
||||
} else {
|
||||
_, height = element.MinimumSize()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// OnFlexibleHeightChange sets a function to be called when the parameters
|
||||
// affecting this element's flexible height are changed.
|
||||
func (element *Label) OnFlexibleHeightChange (callback func ()) {
|
||||
element.onFlexibleHeightChange = callback
|
||||
}
|
||||
|
||||
// SetText sets the label's text.
|
||||
func (element *Label) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
// SetWrap sets wether or not the label's text wraps. If the text is set to
|
||||
// wrap, the element will have a minimum size of a single character and
|
||||
// automatically wrap its text. If the text is set to not wrap, the element will
|
||||
// have a minimum size that fits its text.
|
||||
func (element *Label) SetWrap (wrap bool) {
|
||||
if wrap == element.wrap { return }
|
||||
if !wrap {
|
||||
element.drawer.SetMaxWidth(0)
|
||||
element.drawer.SetMaxHeight(0)
|
||||
}
|
||||
element.wrap = wrap
|
||||
element.updateMinimumSize()
|
||||
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Label) updateMinimumSize () {
|
||||
if element.wrap {
|
||||
em := element.drawer.Em().Round()
|
||||
if em < 1 { em = theme.Padding() }
|
||||
element.core.SetMinimumSize (
|
||||
em, element.drawer.LineHeight().Round())
|
||||
if element.onFlexibleHeightChange != nil {
|
||||
element.onFlexibleHeightChange()
|
||||
}
|
||||
} else {
|
||||
bounds := element.drawer.LayoutBounds()
|
||||
element.core.SetMinimumSize(bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Label) draw () {
|
||||
bounds := element.Bounds()
|
||||
|
||||
pattern, _ := theme.BackgroundPattern(theme.PatternState {
|
||||
Case: labelCase,
|
||||
})
|
||||
artist.FillRectangle(element, pattern, bounds)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
|
||||
foreground, _ := theme.ForegroundPattern (theme.PatternState {
|
||||
Case: labelCase,
|
||||
})
|
||||
element.drawer.Draw (element, foreground, bounds.Min.Sub(textBounds.Min))
|
||||
}
|
|
@ -1,396 +0,0 @@
|
|||
package basic
|
||||
|
||||
import "fmt"
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
var listCase = theme.C("basic", "list")
|
||||
|
||||
// List is an element that contains several objects that a user can select.
|
||||
type List struct {
|
||||
*core.Core
|
||||
*core.FocusableCore
|
||||
core core.CoreControl
|
||||
focusableControl core.FocusableCoreControl
|
||||
|
||||
pressed bool
|
||||
|
||||
contentHeight int
|
||||
forcedMinimumWidth int
|
||||
forcedMinimumHeight int
|
||||
|
||||
selectedEntry int
|
||||
scroll int
|
||||
entries []ListEntry
|
||||
|
||||
onScrollBoundsChange func ()
|
||||
onNoEntrySelected func ()
|
||||
}
|
||||
|
||||
// NewList creates a new list element with the specified entries.
|
||||
func NewList (entries ...ListEntry) (element *List) {
|
||||
element = &List { selectedEntry: -1 }
|
||||
element.Core, element.core = core.NewCore(element.handleResize)
|
||||
element.FocusableCore,
|
||||
element.focusableControl = core.NewFocusableCore (func () {
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
})
|
||||
|
||||
element.entries = make([]ListEntry, len(entries))
|
||||
for index, entry := range entries {
|
||||
element.entries[index] = entry
|
||||
}
|
||||
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
func (element *List) handleResize () {
|
||||
for index, entry := range element.entries {
|
||||
element.entries[index] = element.resizeEntryToFit(entry)
|
||||
}
|
||||
|
||||
element.draw()
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Collapse forces a minimum width and height upon the list. If a zero value is
|
||||
// given for a dimension, its minimum will be determined by the list's content.
|
||||
// If the list's height goes beyond the forced size, it will need to be accessed
|
||||
// via scrolling. If an entry's width goes beyond the forced size, its text will
|
||||
// be truncated so that it fits.
|
||||
func (element *List) Collapse (width, height int) {
|
||||
element.forcedMinimumWidth = width
|
||||
element.forcedMinimumHeight = height
|
||||
element.updateMinimumSize()
|
||||
}
|
||||
|
||||
func (element *List) HandleMouseDown (x, y int, button tomo.Button) {
|
||||
if !element.Enabled() { return }
|
||||
if !element.Focused() { element.Focus() }
|
||||
if button != tomo.ButtonLeft { return }
|
||||
element.pressed = true
|
||||
if element.selectUnderMouse(x, y) && element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *List) HandleMouseUp (x, y int, button tomo.Button) {
|
||||
if button != tomo.ButtonLeft { return }
|
||||
element.pressed = false
|
||||
}
|
||||
|
||||
func (element *List) HandleMouseMove (x, y int) {
|
||||
if element.pressed {
|
||||
if element.selectUnderMouse(x, y) && element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *List) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||
|
||||
func (element *List) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
|
||||
altered := false
|
||||
switch key {
|
||||
case tomo.KeyLeft, tomo.KeyUp:
|
||||
altered = element.changeSelectionBy(-1)
|
||||
|
||||
case tomo.KeyRight, tomo.KeyDown:
|
||||
altered = element.changeSelectionBy(1)
|
||||
|
||||
case tomo.KeyEscape:
|
||||
altered = element.selectEntry(-1)
|
||||
}
|
||||
|
||||
if altered && element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *List) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) { }
|
||||
|
||||
// ScrollContentBounds returns the full content size of the element.
|
||||
func (element *List) ScrollContentBounds () (bounds image.Rectangle) {
|
||||
return image.Rect (
|
||||
0, 0,
|
||||
1, element.contentHeight)
|
||||
}
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's viewport
|
||||
// relative to ScrollBounds.
|
||||
func (element *List) ScrollViewportBounds () (bounds image.Rectangle) {
|
||||
return image.Rect (
|
||||
0, element.scroll,
|
||||
0, element.scroll + element.scrollViewportHeight())
|
||||
}
|
||||
|
||||
// ScrollTo scrolls the viewport to the specified point relative to
|
||||
// ScrollBounds.
|
||||
func (element *List) ScrollTo (position image.Point) {
|
||||
element.scroll = position.Y
|
||||
if element.scroll < 0 {
|
||||
element.scroll = 0
|
||||
} else if element.scroll > element.maxScrollHeight() {
|
||||
element.scroll = element.maxScrollHeight()
|
||||
}
|
||||
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
func (element *List) ScrollAxes () (horizontal, vertical bool) {
|
||||
return false, true
|
||||
}
|
||||
|
||||
func (element *List) scrollViewportHeight () (height int) {
|
||||
_, inset := theme.ListPattern(theme.PatternState {
|
||||
Case: listCase,
|
||||
})
|
||||
return element.Bounds().Dy() - inset[0] - inset[2]
|
||||
}
|
||||
|
||||
func (element *List) maxScrollHeight () (height int) {
|
||||
height =
|
||||
element.contentHeight -
|
||||
element.scrollViewportHeight()
|
||||
if height < 0 { height = 0 }
|
||||
return
|
||||
}
|
||||
|
||||
func (element *List) OnScrollBoundsChange (callback func ()) {
|
||||
element.onScrollBoundsChange = callback
|
||||
}
|
||||
|
||||
// OnNoEntrySelected sets a function to be called when the user chooses to
|
||||
// deselect the current selected entry by clicking on empty space within the
|
||||
// list or by pressing the escape key.
|
||||
func (element *List) OnNoEntrySelected (callback func ()) {
|
||||
element.onNoEntrySelected = callback
|
||||
}
|
||||
|
||||
// CountEntries returns the amount of entries in the list.
|
||||
func (element *List) CountEntries () (count int) {
|
||||
return len(element.entries)
|
||||
}
|
||||
|
||||
// Append adds an entry to the end of the list.
|
||||
func (element *List) Append (entry ListEntry) {
|
||||
// append
|
||||
entry.Collapse(element.forcedMinimumWidth)
|
||||
element.entries = append(element.entries, entry)
|
||||
|
||||
// recalculate, redraw, notify
|
||||
element.updateMinimumSize()
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// EntryAt returns the entry at the specified index. If the index is out of
|
||||
// bounds, it panics.
|
||||
func (element *List) EntryAt (index int) (entry ListEntry) {
|
||||
if index < 0 || index >= len(element.entries) {
|
||||
panic(fmt.Sprint("basic.List.EntryAt index out of range: ", index))
|
||||
}
|
||||
return element.entries[index]
|
||||
}
|
||||
|
||||
// Insert inserts an entry into the list at the speified index. If the index is
|
||||
// out of bounds, it is constrained either to zero or len(entries).
|
||||
func (element *List) Insert (index int, entry ListEntry) {
|
||||
if index < 0 { index = 0 }
|
||||
if index > len(element.entries) { index = len(element.entries) }
|
||||
|
||||
// insert
|
||||
element.entries = append (
|
||||
element.entries[:index + 1],
|
||||
element.entries[index:]...)
|
||||
entry.Collapse(element.forcedMinimumWidth)
|
||||
element.entries[index] = entry
|
||||
|
||||
// recalculate, redraw, notify
|
||||
element.updateMinimumSize()
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removes the entry at the specified index. If the index is out of
|
||||
// bounds, it panics.
|
||||
func (element *List) Remove (index int) {
|
||||
if index < 0 || index >= len(element.entries) {
|
||||
panic(fmt.Sprint("basic.List.Remove index out of range: ", index))
|
||||
}
|
||||
|
||||
// delete
|
||||
element.entries = append (
|
||||
element.entries[:index],
|
||||
element.entries[index + 1:]...)
|
||||
|
||||
// recalculate, redraw, notify
|
||||
element.updateMinimumSize()
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Replace replaces the entry at the specified index with another. If the index
|
||||
// is out of bounds, it panics.
|
||||
func (element *List) Replace (index int, entry ListEntry) {
|
||||
if index < 0 || index >= len(element.entries) {
|
||||
panic(fmt.Sprint("basic.List.Replace index out of range: ", index))
|
||||
}
|
||||
|
||||
// replace
|
||||
entry.Collapse(element.forcedMinimumWidth)
|
||||
element.entries[index] = entry
|
||||
|
||||
// redraw
|
||||
element.updateMinimumSize()
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *List) selectUnderMouse (x, y int) (updated bool) {
|
||||
_, inset := theme.ListPattern(theme.PatternState { })
|
||||
bounds := inset.Apply(element.Bounds())
|
||||
mousePoint := image.Pt(x, y)
|
||||
dot := image.Pt (
|
||||
bounds.Min.X,
|
||||
bounds.Min.Y - element.scroll)
|
||||
|
||||
newlySelectedEntryIndex := -1
|
||||
for index, entry := range element.entries {
|
||||
entryPosition := dot
|
||||
dot.Y += entry.Bounds().Dy()
|
||||
if entryPosition.Y > bounds.Max.Y { break }
|
||||
if mousePoint.In(entry.Bounds().Add(entryPosition)) {
|
||||
newlySelectedEntryIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return element.selectEntry(newlySelectedEntryIndex)
|
||||
}
|
||||
|
||||
func (element *List) selectEntry (index int) (updated bool) {
|
||||
if element.selectedEntry == index { return false }
|
||||
element.selectedEntry = index
|
||||
if element.selectedEntry < 0 {
|
||||
if element.onNoEntrySelected != nil {
|
||||
element.onNoEntrySelected()
|
||||
}
|
||||
} else {
|
||||
element.entries[element.selectedEntry].RunSelect()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (element *List) changeSelectionBy (delta int) (updated bool) {
|
||||
newIndex := element.selectedEntry + delta
|
||||
if newIndex < 0 { newIndex = len(element.entries) - 1 }
|
||||
if newIndex >= len(element.entries) { newIndex = 0 }
|
||||
return element.selectEntry(newIndex)
|
||||
}
|
||||
|
||||
func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) {
|
||||
_, inset := theme.ListPattern(theme.PatternState {
|
||||
Case: listCase,
|
||||
})
|
||||
entry.Collapse(element.forcedMinimumWidth - inset[3] - inset[1])
|
||||
return entry
|
||||
}
|
||||
|
||||
func (element *List) updateMinimumSize () {
|
||||
element.contentHeight = 0
|
||||
for _, entry := range element.entries {
|
||||
element.contentHeight += entry.Bounds().Dy()
|
||||
}
|
||||
|
||||
minimumWidth := element.forcedMinimumWidth
|
||||
minimumHeight := element.forcedMinimumHeight
|
||||
|
||||
if minimumWidth == 0 {
|
||||
for _, entry := range element.entries {
|
||||
entryWidth := entry.Bounds().Dx()
|
||||
if entryWidth > minimumWidth {
|
||||
minimumWidth = entryWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if minimumHeight == 0 {
|
||||
minimumHeight = element.contentHeight
|
||||
}
|
||||
|
||||
_, inset := theme.ListPattern(theme.PatternState {
|
||||
Case: listCase,
|
||||
})
|
||||
minimumHeight += inset[0] + inset[2]
|
||||
|
||||
element.core.SetMinimumSize(minimumWidth, minimumHeight)
|
||||
}
|
||||
|
||||
func (element *List) draw () {
|
||||
bounds := element.Bounds()
|
||||
|
||||
pattern, inset := theme.ListPattern(theme.PatternState {
|
||||
Case: listCase,
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.Focused(),
|
||||
})
|
||||
artist.FillRectangle(element, pattern, bounds)
|
||||
|
||||
bounds = inset.Apply(bounds)
|
||||
dot := image.Point {
|
||||
bounds.Min.X,
|
||||
bounds.Min.Y - element.scroll,
|
||||
}
|
||||
innerCanvas := tomo.Cut(element, bounds)
|
||||
for index, entry := range element.entries {
|
||||
entryPosition := dot
|
||||
dot.Y += entry.Bounds().Dy()
|
||||
if dot.Y < bounds.Min.Y { continue }
|
||||
if entryPosition.Y > bounds.Max.Y { break }
|
||||
entry.Draw (
|
||||
innerCanvas, entryPosition,
|
||||
element.Focused(), element.selectedEntry == index)
|
||||
}
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
package basic
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
|
||||
var listEntryCase = theme.C("basic", "listEntry")
|
||||
|
||||
// ListEntry is an item that can be added to a list.
|
||||
type ListEntry struct {
|
||||
drawer artist.TextDrawer
|
||||
bounds image.Rectangle
|
||||
textPoint image.Point
|
||||
text string
|
||||
forcedMinimumWidth int
|
||||
onSelect func ()
|
||||
}
|
||||
|
||||
func NewListEntry (text string, onSelect func ()) (entry ListEntry) {
|
||||
entry = ListEntry {
|
||||
text: text,
|
||||
onSelect: onSelect,
|
||||
}
|
||||
entry.drawer.SetText([]rune(text))
|
||||
entry.drawer.SetFace(theme.FontFaceRegular())
|
||||
entry.updateBounds()
|
||||
return
|
||||
}
|
||||
|
||||
func (entry *ListEntry) Collapse (width int) {
|
||||
if entry.forcedMinimumWidth == width { return }
|
||||
entry.forcedMinimumWidth = width
|
||||
entry.updateBounds()
|
||||
}
|
||||
|
||||
func (entry *ListEntry) updateBounds () {
|
||||
entry.bounds = image.Rectangle { }
|
||||
entry.bounds.Max.Y = entry.drawer.LineHeight().Round()
|
||||
if entry.forcedMinimumWidth > 0 {
|
||||
entry.bounds.Max.X = entry.forcedMinimumWidth
|
||||
} else {
|
||||
entry.bounds.Max.X = entry.drawer.LayoutBounds().Dx()
|
||||
}
|
||||
|
||||
_, inset := theme.ItemPattern(theme.PatternState {
|
||||
})
|
||||
entry.bounds.Max.Y += inset[0] + inset[2]
|
||||
|
||||
entry.textPoint =
|
||||
image.Pt(inset[3], inset[0]).
|
||||
Sub(entry.drawer.LayoutBounds().Min)
|
||||
}
|
||||
|
||||
func (entry *ListEntry) Draw (
|
||||
destination tomo.Canvas,
|
||||
offset image.Point,
|
||||
focused bool,
|
||||
on bool,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
pattern, _ := theme.ItemPattern(theme.PatternState {
|
||||
Case: listEntryCase,
|
||||
Focused: focused,
|
||||
On: on,
|
||||
})
|
||||
artist.FillRectangle (
|
||||
destination,
|
||||
pattern,
|
||||
entry.Bounds().Add(offset))
|
||||
foreground, _ := theme.ForegroundPattern (theme.PatternState {
|
||||
Case: listEntryCase,
|
||||
Focused: focused,
|
||||
On: on,
|
||||
})
|
||||
return entry.drawer.Draw (
|
||||
destination,
|
||||
foreground,
|
||||
offset.Add(entry.textPoint))
|
||||
}
|
||||
|
||||
func (entry *ListEntry) RunSelect () {
|
||||
if entry.onSelect != nil {
|
||||
entry.onSelect()
|
||||
}
|
||||
}
|
||||
|
||||
func (entry *ListEntry) Bounds () (bounds image.Rectangle) {
|
||||
return entry.bounds
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
package basic
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
// ProgressBar displays a visual indication of how far along a task is.
|
||||
type ProgressBar struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
progress float64
|
||||
}
|
||||
|
||||
// NewProgressBar creates a new progress bar displaying the given progress
|
||||
// level.
|
||||
func NewProgressBar (progress float64) (element *ProgressBar) {
|
||||
element = &ProgressBar { progress: progress }
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
element.core.SetMinimumSize(theme.Padding() * 2, theme.Padding() * 2)
|
||||
return
|
||||
}
|
||||
|
||||
// SetProgress sets the progress level of the bar.
|
||||
func (element *ProgressBar) SetProgress (progress float64) {
|
||||
if progress == element.progress { return }
|
||||
element.progress = progress
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ProgressBar) draw () {
|
||||
bounds := element.Bounds()
|
||||
|
||||
pattern, inset := theme.SunkenPattern(theme.PatternState { })
|
||||
artist.FillRectangle(element, pattern, bounds)
|
||||
bounds = inset.Apply(bounds)
|
||||
meterBounds := image.Rect (
|
||||
bounds.Min.X, bounds.Min.Y,
|
||||
bounds.Min.X + int(float64(bounds.Dx()) * element.progress),
|
||||
bounds.Max.Y)
|
||||
accent, _ := theme.AccentPattern(theme.PatternState { })
|
||||
artist.FillRectangle(element, accent, meterBounds)
|
||||
}
|
|
@ -1,473 +0,0 @@
|
|||
package basic
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
var scrollContainerCase = theme.C("basic", "scrollContainer")
|
||||
var scrollBarHorizontalCase = theme.C("basic", "scrollBarHorizontal")
|
||||
var scrollBarVerticalCase = theme.C("basic", "scrollBarVertical")
|
||||
|
||||
// ScrollContainer is a container that is capable of holding a scrollable
|
||||
// element.
|
||||
type ScrollContainer struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
focused bool
|
||||
|
||||
child tomo.Scrollable
|
||||
childWidth, childHeight int
|
||||
|
||||
horizontal struct {
|
||||
exists bool
|
||||
enabled bool
|
||||
dragging bool
|
||||
dragOffset int
|
||||
gutter image.Rectangle
|
||||
track image.Rectangle
|
||||
bar image.Rectangle
|
||||
}
|
||||
|
||||
vertical struct {
|
||||
exists bool
|
||||
enabled bool
|
||||
dragging bool
|
||||
dragOffset int
|
||||
gutter image.Rectangle
|
||||
track image.Rectangle
|
||||
bar image.Rectangle
|
||||
}
|
||||
|
||||
onFocusRequest func () (granted bool)
|
||||
onFocusMotionRequest func (tomo.KeynavDirection) (granted bool)
|
||||
}
|
||||
|
||||
// NewScrollContainer creates a new scroll container with the specified scroll
|
||||
// bars.
|
||||
func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) {
|
||||
element = &ScrollContainer { }
|
||||
element.Core, element.core = core.NewCore(element.handleResize)
|
||||
element.updateMinimumSize()
|
||||
element.horizontal.exists = horizontal
|
||||
element.vertical.exists = vertical
|
||||
return
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) handleResize () {
|
||||
element.recalculate()
|
||||
element.resizeChildToFit()
|
||||
element.draw()
|
||||
}
|
||||
|
||||
// Adopt adds a scrollable element to the scroll container. The container can
|
||||
// only contain one scrollable element at a time, and when a new one is adopted
|
||||
// it replaces the last one.
|
||||
func (element *ScrollContainer) Adopt (child tomo.Scrollable) {
|
||||
// disown previous child if it exists
|
||||
if element.child != nil {
|
||||
element.clearChildEventHandlers(child)
|
||||
}
|
||||
|
||||
// adopt new child
|
||||
element.child = child
|
||||
if child != nil {
|
||||
child.OnDamage(element.childDamageCallback)
|
||||
child.OnMinimumSizeChange(element.updateMinimumSize)
|
||||
child.OnScrollBoundsChange(element.childScrollBoundsChangeCallback)
|
||||
if newChild, ok := child.(tomo.Focusable); ok {
|
||||
newChild.OnFocusRequest (
|
||||
element.childFocusRequestCallback)
|
||||
newChild.OnFocusMotionRequest (
|
||||
element.childFocusMotionRequestCallback)
|
||||
}
|
||||
|
||||
// TODO: somehow inform the core that we do not in fact want to
|
||||
// redraw the element.
|
||||
element.updateMinimumSize()
|
||||
|
||||
element.horizontal.enabled,
|
||||
element.vertical.enabled = element.child.ScrollAxes()
|
||||
|
||||
if element.core.HasImage() {
|
||||
element.resizeChildToFit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) {
|
||||
if child, ok := element.child.(tomo.KeyboardTarget); ok {
|
||||
child.HandleKeyDown(key, modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) {
|
||||
if child, ok := element.child.(tomo.KeyboardTarget); ok {
|
||||
child.HandleKeyUp(key, modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleMouseDown (x, y int, button tomo.Button) {
|
||||
point := image.Pt(x, y)
|
||||
if point.In(element.horizontal.bar) {
|
||||
element.horizontal.dragging = true
|
||||
element.horizontal.dragOffset =
|
||||
x - element.horizontal.bar.Min.X +
|
||||
element.Bounds().Min.X
|
||||
element.dragHorizontalBar(point)
|
||||
|
||||
} else if point.In(element.horizontal.gutter) {
|
||||
// FIXME: x backend and scroll container should pull these
|
||||
// values from the same place
|
||||
if x > element.horizontal.bar.Min.X {
|
||||
element.scrollChildBy(16, 0)
|
||||
} else {
|
||||
element.scrollChildBy(-16, 0)
|
||||
}
|
||||
|
||||
} else if point.In(element.vertical.bar) {
|
||||
element.vertical.dragging = true
|
||||
element.vertical.dragOffset =
|
||||
y - element.vertical.bar.Min.Y +
|
||||
element.Bounds().Min.Y
|
||||
element.dragVerticalBar(point)
|
||||
|
||||
} else if point.In(element.vertical.gutter) {
|
||||
if y > element.vertical.bar.Min.Y {
|
||||
element.scrollChildBy(0, 16)
|
||||
} else {
|
||||
element.scrollChildBy(0, -16)
|
||||
}
|
||||
|
||||
} else if child, ok := element.child.(tomo.MouseTarget); ok {
|
||||
child.HandleMouseDown(x, y, button)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleMouseUp (x, y int, button tomo.Button) {
|
||||
if element.horizontal.dragging {
|
||||
element.horizontal.dragging = false
|
||||
element.drawHorizontalBar()
|
||||
element.core.DamageRegion(element.horizontal.bar)
|
||||
|
||||
} else if element.vertical.dragging {
|
||||
element.vertical.dragging = false
|
||||
element.drawVerticalBar()
|
||||
element.core.DamageRegion(element.vertical.bar)
|
||||
|
||||
} else if child, ok := element.child.(tomo.MouseTarget); ok {
|
||||
child.HandleMouseUp(x, y, button)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleMouseMove (x, y int) {
|
||||
if element.horizontal.dragging {
|
||||
element.dragHorizontalBar(image.Pt(x, y))
|
||||
|
||||
} else if element.vertical.dragging {
|
||||
element.dragVerticalBar(image.Pt(x, y))
|
||||
|
||||
} else if child, ok := element.child.(tomo.MouseTarget); ok {
|
||||
child.HandleMouseMove(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleMouseScroll (
|
||||
x, y int,
|
||||
deltaX, deltaY float64,
|
||||
) {
|
||||
element.scrollChildBy(int(deltaX), int(deltaY))
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) scrollChildBy (x, y int) {
|
||||
if element.child == nil { return }
|
||||
scrollPoint :=
|
||||
element.child.ScrollViewportBounds().Min.
|
||||
Add(image.Pt(x, y))
|
||||
element.child.ScrollTo(scrollPoint)
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) Focused () (focused bool) {
|
||||
return element.focused
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) Focus () {
|
||||
if element.onFocusRequest != nil {
|
||||
element.onFocusRequest()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleFocus (
|
||||
direction tomo.KeynavDirection,
|
||||
) (
|
||||
accepted bool,
|
||||
) {
|
||||
if child, ok := element.child.(tomo.Focusable); ok {
|
||||
element.focused = true
|
||||
return child.HandleFocus(direction)
|
||||
} else {
|
||||
element.focused = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleUnfocus () {
|
||||
if child, ok := element.child.(tomo.Focusable); ok {
|
||||
child.HandleUnfocus()
|
||||
}
|
||||
element.focused = false
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) OnFocusRequest (callback func () (granted bool)) {
|
||||
element.onFocusRequest = callback
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) OnFocusMotionRequest (
|
||||
callback func (direction tomo.KeynavDirection) (granted bool),
|
||||
) {
|
||||
element.onFocusMotionRequest = callback
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) childDamageCallback (region tomo.Canvas) {
|
||||
element.core.DamageRegion(artist.Paste(element, region, image.Point { }))
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) childFocusRequestCallback () (granted bool) {
|
||||
child, ok := element.child.(tomo.Focusable)
|
||||
if !ok { return false }
|
||||
if element.onFocusRequest != nil && element.onFocusRequest() {
|
||||
child.HandleFocus(tomo.KeynavDirectionNeutral)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) childFocusMotionRequestCallback (
|
||||
direction tomo.KeynavDirection,
|
||||
) (
|
||||
granted bool,
|
||||
) {
|
||||
if element.onFocusMotionRequest == nil { return }
|
||||
return element.onFocusMotionRequest(direction)
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) clearChildEventHandlers (child tomo.Scrollable) {
|
||||
child.DrawTo(nil)
|
||||
child.OnDamage(nil)
|
||||
child.OnMinimumSizeChange(nil)
|
||||
child.OnScrollBoundsChange(nil)
|
||||
if child0, ok := child.(tomo.Focusable); ok {
|
||||
child0.OnFocusRequest(nil)
|
||||
child0.OnFocusMotionRequest(nil)
|
||||
if child0.Focused() {
|
||||
child0.HandleUnfocus()
|
||||
}
|
||||
}
|
||||
if child0, ok := child.(tomo.Flexible); ok {
|
||||
child0.OnFlexibleHeightChange(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) resizeChildToFit () {
|
||||
childBounds := image.Rect (
|
||||
0, 0,
|
||||
element.childWidth,
|
||||
element.childHeight).Add(element.Bounds().Min)
|
||||
element.child.DrawTo(tomo.Cut(element, childBounds))
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) recalculate () {
|
||||
_, gutterInsetHorizontal := theme.GutterPattern(theme.PatternState {
|
||||
Case: scrollBarHorizontalCase,
|
||||
})
|
||||
_, gutterInsetVertical := theme.GutterPattern(theme.PatternState {
|
||||
Case: scrollBarHorizontalCase,
|
||||
})
|
||||
|
||||
horizontal := &element.horizontal
|
||||
vertical := &element.vertical
|
||||
bounds := element.Bounds()
|
||||
thicknessHorizontal :=
|
||||
theme.HandleWidth() +
|
||||
gutterInsetHorizontal[3] +
|
||||
gutterInsetHorizontal[1]
|
||||
thicknessVertical :=
|
||||
theme.HandleWidth() +
|
||||
gutterInsetVertical[3] +
|
||||
gutterInsetVertical[1]
|
||||
|
||||
// calculate child size
|
||||
element.childWidth = bounds.Dx()
|
||||
element.childHeight = bounds.Dy()
|
||||
|
||||
// reset bounds
|
||||
horizontal.gutter = image.Rectangle { }
|
||||
vertical.gutter = image.Rectangle { }
|
||||
horizontal.bar = image.Rectangle { }
|
||||
vertical.bar = image.Rectangle { }
|
||||
|
||||
// if enabled, give substance to the gutters
|
||||
if horizontal.exists {
|
||||
horizontal.gutter.Min.X = bounds.Min.X
|
||||
horizontal.gutter.Min.Y = bounds.Max.Y - thicknessHorizontal
|
||||
horizontal.gutter.Max.X = bounds.Max.X
|
||||
horizontal.gutter.Max.Y = bounds.Max.Y
|
||||
if vertical.exists {
|
||||
horizontal.gutter.Max.X -= thicknessVertical
|
||||
}
|
||||
element.childHeight -= thicknessHorizontal
|
||||
horizontal.track = gutterInsetHorizontal.Apply(horizontal.gutter)
|
||||
}
|
||||
if vertical.exists {
|
||||
vertical.gutter.Min.X = bounds.Max.X - thicknessVertical
|
||||
vertical.gutter.Max.X = bounds.Max.X
|
||||
vertical.gutter.Min.Y = bounds.Min.Y
|
||||
vertical.gutter.Max.Y = bounds.Max.Y
|
||||
if horizontal.exists {
|
||||
vertical.gutter.Max.Y -= thicknessHorizontal
|
||||
}
|
||||
element.childWidth -= thicknessVertical
|
||||
vertical.track = gutterInsetVertical.Apply(vertical.gutter)
|
||||
}
|
||||
|
||||
// if enabled, calculate the positions of the bars
|
||||
contentBounds := element.child.ScrollContentBounds()
|
||||
viewportBounds := element.child.ScrollViewportBounds()
|
||||
if horizontal.exists && horizontal.enabled {
|
||||
horizontal.bar.Min.Y = horizontal.track.Min.Y
|
||||
horizontal.bar.Max.Y = horizontal.track.Max.Y
|
||||
|
||||
scale := float64(horizontal.track.Dx()) /
|
||||
float64(contentBounds.Dx())
|
||||
horizontal.bar.Min.X = int(float64(viewportBounds.Min.X) * scale)
|
||||
horizontal.bar.Max.X = int(float64(viewportBounds.Max.X) * scale)
|
||||
|
||||
horizontal.bar.Min.X += horizontal.track.Min.X
|
||||
horizontal.bar.Max.X += horizontal.track.Min.X
|
||||
}
|
||||
if vertical.exists && vertical.enabled {
|
||||
vertical.bar.Min.X = vertical.track.Min.X
|
||||
vertical.bar.Max.X = vertical.track.Max.X
|
||||
|
||||
scale := float64(vertical.track.Dy()) /
|
||||
float64(contentBounds.Dy())
|
||||
vertical.bar.Min.Y = int(float64(viewportBounds.Min.Y) * scale)
|
||||
vertical.bar.Max.Y = int(float64(viewportBounds.Max.Y) * scale)
|
||||
|
||||
vertical.bar.Min.Y += vertical.track.Min.Y
|
||||
vertical.bar.Max.Y += vertical.track.Min.Y
|
||||
}
|
||||
|
||||
// if the scroll bars are out of bounds, don't display them.
|
||||
if horizontal.bar.Dx() >= horizontal.track.Dx() {
|
||||
horizontal.bar = image.Rectangle { }
|
||||
}
|
||||
if vertical.bar.Dy() >= vertical.track.Dy() {
|
||||
vertical.bar = image.Rectangle { }
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) draw () {
|
||||
artist.Paste(element, element.child, image.Point { })
|
||||
deadPattern, _ := theme.DeadPattern(theme.PatternState {
|
||||
Case: scrollContainerCase,
|
||||
})
|
||||
artist.FillRectangle (
|
||||
element, deadPattern,
|
||||
image.Rect (
|
||||
element.vertical.gutter.Min.X,
|
||||
element.horizontal.gutter.Min.Y,
|
||||
element.vertical.gutter.Max.X,
|
||||
element.horizontal.gutter.Max.Y))
|
||||
element.drawHorizontalBar()
|
||||
element.drawVerticalBar()
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) drawHorizontalBar () {
|
||||
gutterPattern, _ := theme.GutterPattern (theme.PatternState {
|
||||
Case: scrollBarHorizontalCase,
|
||||
Disabled: !element.horizontal.enabled,
|
||||
})
|
||||
artist.FillRectangle(element, gutterPattern, element.horizontal.gutter)
|
||||
|
||||
handlePattern, _ := theme.HandlePattern (theme.PatternState {
|
||||
Case: scrollBarHorizontalCase,
|
||||
Disabled: !element.horizontal.enabled,
|
||||
Pressed: element.horizontal.dragging,
|
||||
})
|
||||
artist.FillRectangle(element, handlePattern, element.horizontal.bar)
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) drawVerticalBar () {
|
||||
gutterPattern, _ := theme.GutterPattern (theme.PatternState {
|
||||
Case: scrollBarVerticalCase,
|
||||
Disabled: !element.vertical.enabled,
|
||||
})
|
||||
artist.FillRectangle(element, gutterPattern, element.vertical.gutter)
|
||||
|
||||
handlePattern, _ := theme.HandlePattern (theme.PatternState {
|
||||
Case: scrollBarVerticalCase,
|
||||
Disabled: !element.vertical.enabled,
|
||||
Pressed: element.vertical.dragging,
|
||||
})
|
||||
artist.FillRectangle(element, handlePattern, element.vertical.bar)
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) dragHorizontalBar (mousePosition image.Point) {
|
||||
scrollX :=
|
||||
float64(element.child.ScrollContentBounds().Dx()) /
|
||||
float64(element.horizontal.track.Dx()) *
|
||||
float64(mousePosition.X - element.horizontal.dragOffset)
|
||||
scrollY := element.child.ScrollViewportBounds().Min.Y
|
||||
element.child.ScrollTo(image.Pt(int(scrollX), scrollY))
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) dragVerticalBar (mousePosition image.Point) {
|
||||
scrollY :=
|
||||
float64(element.child.ScrollContentBounds().Dy()) /
|
||||
float64(element.vertical.track.Dy()) *
|
||||
float64(mousePosition.Y - element.vertical.dragOffset)
|
||||
scrollX := element.child.ScrollViewportBounds().Min.X
|
||||
element.child.ScrollTo(image.Pt(scrollX, int(scrollY)))
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) updateMinimumSize () {
|
||||
_, gutterInsetHorizontal := theme.GutterPattern(theme.PatternState {
|
||||
Case: scrollBarHorizontalCase,
|
||||
})
|
||||
_, gutterInsetVertical := theme.GutterPattern(theme.PatternState {
|
||||
Case: scrollBarHorizontalCase,
|
||||
})
|
||||
|
||||
thicknessHorizontal :=
|
||||
theme.HandleWidth() +
|
||||
gutterInsetHorizontal[3] +
|
||||
gutterInsetHorizontal[1]
|
||||
thicknessVertical :=
|
||||
theme.HandleWidth() +
|
||||
gutterInsetVertical[3] +
|
||||
gutterInsetVertical[1]
|
||||
|
||||
width := thicknessHorizontal
|
||||
height := thicknessVertical
|
||||
if element.child != nil {
|
||||
childWidth, childHeight := element.child.MinimumSize()
|
||||
width += childWidth
|
||||
height += childHeight
|
||||
}
|
||||
element.core.SetMinimumSize(width, height)
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) childScrollBoundsChangeCallback () {
|
||||
element.horizontal.enabled,
|
||||
element.vertical.enabled = element.child.ScrollAxes()
|
||||
if element.core.HasImage() {
|
||||
element.recalculate()
|
||||
element.drawHorizontalBar()
|
||||
element.drawVerticalBar()
|
||||
element.core.DamageRegion(element.horizontal.gutter)
|
||||
element.core.DamageRegion(element.vertical.gutter)
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
package basic
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
var spacerCase = theme.C("basic", "spacer")
|
||||
|
||||
// Spacer can be used to put space between two elements..
|
||||
type Spacer struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
line bool
|
||||
}
|
||||
|
||||
// NewSpacer creates a new spacer. If line is set to true, the spacer will be
|
||||
// filled with a line color, and if compressed to its minimum width or height,
|
||||
// will appear as a line.
|
||||
func NewSpacer (line bool) (element *Spacer) {
|
||||
element = &Spacer { line: line }
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
element.core.SetMinimumSize(1, 1)
|
||||
return
|
||||
}
|
||||
|
||||
/// SetLine sets whether or not the spacer will appear as a colored line.
|
||||
func (element *Spacer) SetLine (line bool) {
|
||||
if element.line == line { return }
|
||||
element.line = line
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Spacer) draw () {
|
||||
bounds := element.Bounds()
|
||||
|
||||
if element.line {
|
||||
pattern, _ := theme.ForegroundPattern(theme.PatternState {
|
||||
Case: spacerCase,
|
||||
Disabled: true,
|
||||
})
|
||||
artist.FillRectangle(element, pattern, bounds)
|
||||
} else {
|
||||
pattern, _ := theme.BackgroundPattern(theme.PatternState {
|
||||
Case: spacerCase,
|
||||
Disabled: true,
|
||||
})
|
||||
artist.FillRectangle(element, pattern, bounds)
|
||||
}
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
package basic
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
var switchCase = theme.C("basic", "switch")
|
||||
|
||||
// Switch is a toggle-able on/off switch with an optional label. It is
|
||||
// functionally identical to Checkbox, but plays a different semantic role.
|
||||
type Switch struct {
|
||||
*core.Core
|
||||
*core.FocusableCore
|
||||
core core.CoreControl
|
||||
focusableControl core.FocusableCoreControl
|
||||
drawer artist.TextDrawer
|
||||
|
||||
pressed bool
|
||||
checked bool
|
||||
text string
|
||||
|
||||
onToggle func ()
|
||||
}
|
||||
|
||||
// NewSwitch creates a new switch with the specified label text.
|
||||
func NewSwitch (text string, on bool) (element *Switch) {
|
||||
element = &Switch { checked: on, text: text }
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
element.FocusableCore,
|
||||
element.focusableControl = core.NewFocusableCore (func () {
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
})
|
||||
element.drawer.SetFace(theme.FontFaceRegular())
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.calculateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Switch) HandleMouseDown (x, y int, button tomo.Button) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
element.pressed = true
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Switch) HandleMouseUp (x, y int, button tomo.Button) {
|
||||
if button != tomo.ButtonLeft || !element.pressed { return }
|
||||
|
||||
element.pressed = false
|
||||
within := image.Point { x, y }.
|
||||
In(element.Bounds())
|
||||
if within {
|
||||
element.checked = !element.checked
|
||||
}
|
||||
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if within && element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Switch) HandleMouseMove (x, y int) { }
|
||||
func (element *Switch) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||
|
||||
func (element *Switch) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) {
|
||||
if key == tomo.KeyEnter {
|
||||
element.pressed = true
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Switch) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) {
|
||||
if key == tomo.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
element.checked = !element.checked
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnToggle sets the function to be called when the switch is flipped.
|
||||
func (element *Switch) OnToggle (callback func ()) {
|
||||
element.onToggle = callback
|
||||
}
|
||||
|
||||
// Value reports whether or not the switch is currently on.
|
||||
func (element *Switch) Value () (on bool) {
|
||||
return element.checked
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this switch can be flipped or not.
|
||||
func (element *Switch) SetEnabled (enabled bool) {
|
||||
element.focusableControl.SetEnabled(enabled)
|
||||
}
|
||||
|
||||
// SetText sets the checkbox's label text.
|
||||
func (element *Switch) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.calculateMinimumSize()
|
||||
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Switch) calculateMinimumSize () {
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
lineHeight := element.drawer.LineHeight().Round()
|
||||
|
||||
if element.text == "" {
|
||||
element.core.SetMinimumSize(lineHeight * 2, lineHeight)
|
||||
} else {
|
||||
element.core.SetMinimumSize (
|
||||
lineHeight * 2 + theme.Padding() + textBounds.Dx(),
|
||||
lineHeight)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Switch) draw () {
|
||||
bounds := element.Bounds()
|
||||
handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
|
||||
gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()).Add(bounds.Min)
|
||||
backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState {
|
||||
Case: switchCase,
|
||||
})
|
||||
artist.FillRectangle (element, backgroundPattern, bounds)
|
||||
|
||||
if element.checked {
|
||||
handleBounds.Min.X += bounds.Dy()
|
||||
handleBounds.Max.X += bounds.Dy()
|
||||
if element.pressed {
|
||||
handleBounds.Min.X -= 2
|
||||
handleBounds.Max.X -= 2
|
||||
}
|
||||
} else {
|
||||
if element.pressed {
|
||||
handleBounds.Min.X += 2
|
||||
handleBounds.Max.X += 2
|
||||
}
|
||||
}
|
||||
|
||||
gutterPattern, _ := theme.GutterPattern(theme.PatternState {
|
||||
Case: switchCase,
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.Focused(),
|
||||
Pressed: element.pressed,
|
||||
})
|
||||
artist.FillRectangle(element, gutterPattern, gutterBounds)
|
||||
|
||||
handlePattern, _ := theme.HandlePattern(theme.PatternState {
|
||||
Case: switchCase,
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.Focused(),
|
||||
Pressed: element.pressed,
|
||||
})
|
||||
artist.FillRectangle(element, handlePattern, handleBounds)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
offset := bounds.Min.Add(image.Point {
|
||||
X: bounds.Dy() * 2 + theme.Padding(),
|
||||
})
|
||||
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
|
||||
foreground, _ := theme.ForegroundPattern (theme.PatternState {
|
||||
Case: switchCase,
|
||||
Disabled: !element.Enabled(),
|
||||
})
|
||||
element.drawer.Draw(element, foreground, offset)
|
||||
}
|
|
@ -1,334 +0,0 @@
|
|||
package basic
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/textmanip"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
var textBoxCase = theme.C("basic", "textBox")
|
||||
|
||||
// TextBox is a single-line text input.
|
||||
type TextBox struct {
|
||||
*core.Core
|
||||
*core.FocusableCore
|
||||
core core.CoreControl
|
||||
focusableControl core.FocusableCoreControl
|
||||
|
||||
cursor int
|
||||
scroll int
|
||||
placeholder string
|
||||
text []rune
|
||||
|
||||
placeholderDrawer artist.TextDrawer
|
||||
valueDrawer artist.TextDrawer
|
||||
|
||||
onKeyDown func (key tomo.Key, modifiers tomo.Modifiers) (handled bool)
|
||||
onChange func ()
|
||||
onScrollBoundsChange func ()
|
||||
}
|
||||
|
||||
// NewTextBox creates a new text box with the specified placeholder text, and
|
||||
// a value. When the value is empty, the placeholder will be displayed in gray
|
||||
// text.
|
||||
func NewTextBox (placeholder, value string) (element *TextBox) {
|
||||
element = &TextBox { }
|
||||
element.Core, element.core = core.NewCore(element.handleResize)
|
||||
element.FocusableCore,
|
||||
element.focusableControl = core.NewFocusableCore (func () {
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
})
|
||||
element.placeholderDrawer.SetFace(theme.FontFaceRegular())
|
||||
element.valueDrawer.SetFace(theme.FontFaceRegular())
|
||||
element.placeholder = placeholder
|
||||
element.placeholderDrawer.SetText([]rune(placeholder))
|
||||
element.updateMinimumSize()
|
||||
element.SetValue(value)
|
||||
return
|
||||
}
|
||||
|
||||
func (element *TextBox) handleResize () {
|
||||
element.scrollToCursor()
|
||||
element.draw()
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleMouseDown (x, y int, button tomo.Button) {
|
||||
if !element.Enabled() { return }
|
||||
if !element.Focused() { element.Focus() }
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleMouseUp (x, y int, button tomo.Button) { }
|
||||
func (element *TextBox) HandleMouseMove (x, y int) { }
|
||||
func (element *TextBox) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||
|
||||
func (element *TextBox) HandleKeyDown(key tomo.Key, modifiers tomo.Modifiers) {
|
||||
if element.onKeyDown != nil && element.onKeyDown(key, modifiers) {
|
||||
return
|
||||
}
|
||||
|
||||
scrollMemory := element.scroll
|
||||
altered := true
|
||||
textChanged := false
|
||||
switch {
|
||||
case key == tomo.KeyBackspace:
|
||||
if len(element.text) < 1 { break }
|
||||
element.text, element.cursor = textmanip.Backspace (
|
||||
element.text,
|
||||
element.cursor,
|
||||
modifiers.Control)
|
||||
textChanged = true
|
||||
|
||||
case key == tomo.KeyDelete:
|
||||
if len(element.text) < 1 { break }
|
||||
element.text, element.cursor = textmanip.Delete (
|
||||
element.text,
|
||||
element.cursor,
|
||||
modifiers.Control)
|
||||
textChanged = true
|
||||
|
||||
case key == tomo.KeyLeft:
|
||||
element.cursor = textmanip.MoveLeft (
|
||||
element.text,
|
||||
element.cursor,
|
||||
modifiers.Control)
|
||||
|
||||
case key == tomo.KeyRight:
|
||||
element.cursor = textmanip.MoveRight (
|
||||
element.text,
|
||||
element.cursor,
|
||||
modifiers.Control)
|
||||
|
||||
case key.Printable():
|
||||
element.text, element.cursor = textmanip.Type (
|
||||
element.text,
|
||||
element.cursor,
|
||||
rune(key))
|
||||
textChanged = true
|
||||
|
||||
default:
|
||||
altered = false
|
||||
}
|
||||
|
||||
if textChanged {
|
||||
element.runOnChange()
|
||||
element.valueDrawer.SetText(element.text)
|
||||
}
|
||||
|
||||
if altered {
|
||||
element.scrollToCursor()
|
||||
}
|
||||
|
||||
if (textChanged || scrollMemory != element.scroll) &&
|
||||
element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
|
||||
if altered && element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) { }
|
||||
|
||||
func (element *TextBox) SetPlaceholder (placeholder string) {
|
||||
if element.placeholder == placeholder { return }
|
||||
|
||||
element.placeholder = placeholder
|
||||
element.placeholderDrawer.SetText([]rune(placeholder))
|
||||
|
||||
element.updateMinimumSize()
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) SetValue (text string) {
|
||||
// if element.text == text { return }
|
||||
|
||||
element.text = []rune(text)
|
||||
element.runOnChange()
|
||||
element.valueDrawer.SetText(element.text)
|
||||
if element.cursor > element.valueDrawer.Length() {
|
||||
element.cursor = element.valueDrawer.Length()
|
||||
}
|
||||
element.scrollToCursor()
|
||||
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) Value () (value string) {
|
||||
return string(element.text)
|
||||
}
|
||||
|
||||
func (element *TextBox) Filled () (filled bool) {
|
||||
return len(element.text) > 0
|
||||
}
|
||||
|
||||
func (element *TextBox) OnKeyDown (
|
||||
callback func (key tomo.Key, modifiers tomo.Modifiers) (handled bool),
|
||||
) {
|
||||
element.onKeyDown = callback
|
||||
}
|
||||
|
||||
func (element *TextBox) OnChange (callback func ()) {
|
||||
element.onChange = callback
|
||||
}
|
||||
|
||||
// ScrollContentBounds returns the full content size of the element.
|
||||
func (element *TextBox) ScrollContentBounds () (bounds image.Rectangle) {
|
||||
bounds = element.valueDrawer.LayoutBounds()
|
||||
return bounds.Sub(bounds.Min)
|
||||
}
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's viewport
|
||||
// relative to ScrollBounds.
|
||||
func (element *TextBox) ScrollViewportBounds () (bounds image.Rectangle) {
|
||||
return image.Rect (
|
||||
element.scroll,
|
||||
0,
|
||||
element.scroll + element.scrollViewportWidth(),
|
||||
0)
|
||||
}
|
||||
|
||||
func (element *TextBox) scrollViewportWidth () (width int) {
|
||||
return element.Bounds().Inset(theme.Padding()).Dx()
|
||||
}
|
||||
|
||||
// ScrollTo scrolls the viewport to the specified point relative to
|
||||
// ScrollBounds.
|
||||
func (element *TextBox) ScrollTo (position image.Point) {
|
||||
// constrain to minimum
|
||||
element.scroll = position.X
|
||||
if element.scroll < 0 { element.scroll = 0 }
|
||||
|
||||
// constrain to maximum
|
||||
contentBounds := element.ScrollContentBounds()
|
||||
maxPosition := contentBounds.Max.X - element.scrollViewportWidth()
|
||||
if element.scroll > maxPosition { element.scroll = maxPosition }
|
||||
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
func (element *TextBox) ScrollAxes () (horizontal, vertical bool) {
|
||||
return true, false
|
||||
}
|
||||
|
||||
func (element *TextBox) OnScrollBoundsChange (callback func ()) {
|
||||
element.onScrollBoundsChange = callback
|
||||
}
|
||||
|
||||
func (element *TextBox) updateMinimumSize () {
|
||||
textBounds := element.placeholderDrawer.LayoutBounds()
|
||||
_, inset := theme.InputPattern(theme.PatternState {
|
||||
Case: textBoxCase,
|
||||
})
|
||||
element.core.SetMinimumSize (
|
||||
textBounds.Dx() +
|
||||
theme.Padding() * 2 + inset[3] + inset[1],
|
||||
element.placeholderDrawer.LineHeight().Round() +
|
||||
theme.Padding() * 2 + inset[0] + inset[2])
|
||||
}
|
||||
|
||||
func (element *TextBox) runOnChange () {
|
||||
if element.onChange != nil {
|
||||
element.onChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) scrollToCursor () {
|
||||
if !element.core.HasImage() { return }
|
||||
|
||||
bounds := element.Bounds().Inset(theme.Padding())
|
||||
bounds = bounds.Sub(bounds.Min)
|
||||
bounds.Max.X -= element.valueDrawer.Em().Round()
|
||||
cursorPosition := element.valueDrawer.PositionOf(element.cursor)
|
||||
cursorPosition.X -= element.scroll
|
||||
maxX := bounds.Max.X
|
||||
minX := maxX
|
||||
if cursorPosition.X > maxX {
|
||||
element.scroll += cursorPosition.X - maxX
|
||||
} else if cursorPosition.X < minX {
|
||||
element.scroll -= minX - cursorPosition.X
|
||||
if element.scroll < 0 { element.scroll = 0 }
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) draw () {
|
||||
bounds := element.Bounds()
|
||||
|
||||
// FIXME: take index into account
|
||||
pattern, inset := theme.InputPattern(theme.PatternState {
|
||||
Case: textBoxCase,
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.Focused(),
|
||||
})
|
||||
artist.FillRectangle(element, pattern, bounds)
|
||||
|
||||
if len(element.text) == 0 && !element.Focused() {
|
||||
// draw placeholder
|
||||
textBounds := element.placeholderDrawer.LayoutBounds()
|
||||
offset := bounds.Min.Add (image.Point {
|
||||
X: theme.Padding() + inset[3],
|
||||
Y: theme.Padding() + inset[0],
|
||||
})
|
||||
foreground, _ := theme.ForegroundPattern(theme.PatternState {
|
||||
Case: textBoxCase,
|
||||
Disabled: true,
|
||||
})
|
||||
element.placeholderDrawer.Draw (
|
||||
element,
|
||||
foreground,
|
||||
offset.Sub(textBounds.Min))
|
||||
} else {
|
||||
// draw input value
|
||||
textBounds := element.valueDrawer.LayoutBounds()
|
||||
offset := bounds.Min.Add (image.Point {
|
||||
X: theme.Padding() + inset[3] - element.scroll,
|
||||
Y: theme.Padding() + inset[0],
|
||||
})
|
||||
foreground, _ := theme.ForegroundPattern(theme.PatternState {
|
||||
Case: textBoxCase,
|
||||
Disabled: !element.Enabled(),
|
||||
})
|
||||
element.valueDrawer.Draw (
|
||||
element,
|
||||
foreground,
|
||||
offset.Sub(textBounds.Min))
|
||||
|
||||
if element.Focused() {
|
||||
// cursor
|
||||
cursorPosition := element.valueDrawer.PositionOf (
|
||||
element.cursor)
|
||||
foreground, _ := theme.ForegroundPattern(theme.PatternState {
|
||||
Case: textBoxCase,
|
||||
})
|
||||
artist.Line (
|
||||
element,
|
||||
foreground, 1,
|
||||
cursorPosition.Add(offset),
|
||||
image.Pt (
|
||||
cursorPosition.X,
|
||||
cursorPosition.Y + element.valueDrawer.
|
||||
LineHeight().Round()).Add(offset))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "art"
|
||||
import "art/shatter"
|
||||
|
||||
var boxCase = tomo.C("tomo", "box")
|
||||
|
||||
// Space is a list of spacing configurations that can be passed to some
|
||||
// containers.
|
||||
type Space int; const (
|
||||
SpaceNone Space = 0
|
||||
SpacePadding Space = 1
|
||||
SpaceMargin Space = 2
|
||||
SpaceBoth Space = SpacePadding | SpaceMargin
|
||||
)
|
||||
|
||||
// Includes returns whether a spacing value has been or'd with another spacing
|
||||
// value.
|
||||
func (space Space) Includes (sub Space) bool {
|
||||
return (space & sub) > 0
|
||||
}
|
||||
|
||||
// Box is a container that lays out its children horizontally or vertically.
|
||||
// Child elements can be set to contract to their minimum size, or expand to
|
||||
// fill remaining space. Boxes can be nested and used together to create more
|
||||
// complex layouts.
|
||||
type Box struct {
|
||||
container
|
||||
padding bool
|
||||
margin bool
|
||||
vertical bool
|
||||
}
|
||||
|
||||
// NewHBox creates a new horizontal box.
|
||||
func NewHBox (space Space, children ...tomo.Element) (element *Box) {
|
||||
element = &Box {
|
||||
padding: space.Includes(SpacePadding),
|
||||
margin: space.Includes(SpaceMargin),
|
||||
}
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.minimumSize = element.updateMinimumSize
|
||||
element.init()
|
||||
element.Adopt(children...)
|
||||
return
|
||||
}
|
||||
|
||||
// NewHBox creates a new vertical box.
|
||||
func NewVBox (space Space, children ...tomo.Element) (element *Box) {
|
||||
element = &Box {
|
||||
padding: space.Includes(SpacePadding),
|
||||
margin: space.Includes(SpaceMargin),
|
||||
vertical: true,
|
||||
}
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.minimumSize = element.updateMinimumSize
|
||||
element.init()
|
||||
element.Adopt(children...)
|
||||
return
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Box) Draw (destination art.Canvas) {
|
||||
rocks := make([]image.Rectangle, element.entity.CountChildren())
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
rocks[index] = element.entity.Child(index).Entity().Bounds()
|
||||
}
|
||||
|
||||
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
|
||||
for _, tile := range tiles {
|
||||
element.entity.DrawBackground(art.Cut(destination, tile))
|
||||
}
|
||||
}
|
||||
|
||||
// Layout causes this element to perform a layout operation.
|
||||
func (element *Box) Layout () {
|
||||
margin := element.entity.Theme().Margin(tomo.PatternBackground, boxCase)
|
||||
padding := element.entity.Theme().Padding(tomo.PatternBackground, boxCase)
|
||||
bounds := element.entity.Bounds()
|
||||
if element.padding { bounds = padding.Apply(bounds) }
|
||||
|
||||
var marginSize float64; if element.vertical {
|
||||
marginSize = float64(margin.Y)
|
||||
} else {
|
||||
marginSize = float64(margin.X)
|
||||
}
|
||||
|
||||
freeSpace, nExpanding := element.freeSpace()
|
||||
expandingElementSize := freeSpace / nExpanding
|
||||
|
||||
// set the size and position of each element
|
||||
x := float64(bounds.Min.X)
|
||||
y := float64(bounds.Min.Y)
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
entry := element.scratch[element.entity.Child(index)]
|
||||
|
||||
var size float64; if entry.expand {
|
||||
size = expandingElementSize
|
||||
} else {
|
||||
size = entry.minSize
|
||||
}
|
||||
|
||||
var childBounds image.Rectangle; if element.vertical {
|
||||
childBounds = tomo.Bounds(int(x), int(y), bounds.Dx(), int(size))
|
||||
} else {
|
||||
childBounds = tomo.Bounds(int(x), int(y), int(size), bounds.Dy())
|
||||
}
|
||||
element.entity.PlaceChild(index, childBounds)
|
||||
|
||||
if element.vertical {
|
||||
y += size
|
||||
if element.margin { y += marginSize }
|
||||
} else {
|
||||
x += size
|
||||
if element.margin { x += marginSize }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AdoptExpand adds one or more elements to the box. These elements will be
|
||||
// expanded to fill in empty space.
|
||||
func (element *Box) AdoptExpand (children ...tomo.Element) {
|
||||
element.adopt(true, children...)
|
||||
}
|
||||
|
||||
// DrawBackground draws this element's background pattern to the specified
|
||||
// destination canvas.
|
||||
func (element *Box) DrawBackground (destination art.Canvas) {
|
||||
element.entity.DrawBackground(destination)
|
||||
}
|
||||
|
||||
func (element *Box) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *Box) freeSpace () (space float64, nExpanding float64) {
|
||||
margin := element.entity.Theme().Margin(tomo.PatternBackground, boxCase)
|
||||
padding := element.entity.Theme().Padding(tomo.PatternBackground, boxCase)
|
||||
|
||||
var marginSize int; if element.vertical {
|
||||
marginSize = margin.Y
|
||||
} else {
|
||||
marginSize = margin.X
|
||||
}
|
||||
|
||||
if element.vertical {
|
||||
space = float64(element.entity.Bounds().Dy())
|
||||
} else {
|
||||
space = float64(element.entity.Bounds().Dx())
|
||||
}
|
||||
|
||||
for _, entry := range element.scratch {
|
||||
if entry.expand {
|
||||
nExpanding ++;
|
||||
} else {
|
||||
space -= float64(entry.minSize)
|
||||
}
|
||||
}
|
||||
|
||||
if element.padding {
|
||||
space -= float64(padding.Vertical())
|
||||
}
|
||||
if element.margin {
|
||||
space -= float64(marginSize * (len(element.scratch) - 1))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Box) updateMinimumSize () {
|
||||
margin := element.entity.Theme().Margin(tomo.PatternBackground, boxCase)
|
||||
padding := element.entity.Theme().Padding(tomo.PatternBackground, boxCase)
|
||||
var breadth, size int
|
||||
var marginSize int; if element.vertical {
|
||||
marginSize = margin.Y
|
||||
} else {
|
||||
marginSize = margin.X
|
||||
}
|
||||
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
childWidth, childHeight := element.entity.ChildMinimumSize(index)
|
||||
var childBreadth, childSize int; if element.vertical {
|
||||
childBreadth, childSize = childWidth, childHeight
|
||||
} else {
|
||||
childBreadth, childSize = childHeight, childWidth
|
||||
}
|
||||
|
||||
key := element.entity.Child(index)
|
||||
entry := element.scratch[key]
|
||||
entry.minSize = float64(childSize)
|
||||
element.scratch[key] = entry
|
||||
|
||||
if childBreadth > breadth {
|
||||
breadth = childBreadth
|
||||
}
|
||||
size += childSize
|
||||
if element.margin && index > 0 {
|
||||
size += marginSize
|
||||
}
|
||||
}
|
||||
|
||||
var width, height int; if element.vertical {
|
||||
width, height = breadth, size
|
||||
} else {
|
||||
width, height = size, breadth
|
||||
}
|
||||
|
||||
if element.padding {
|
||||
width += padding.Horizontal()
|
||||
height += padding.Vertical()
|
||||
}
|
||||
|
||||
element.entity.SetMinimumSize(width, height)
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
import "tomo/textdraw"
|
||||
|
||||
var buttonCase = tomo.C("tomo", "button")
|
||||
|
||||
// Button is a clickable button.
|
||||
type Button struct {
|
||||
entity tomo.Entity
|
||||
drawer textdraw.Drawer
|
||||
|
||||
enabled bool
|
||||
pressed bool
|
||||
text string
|
||||
|
||||
showText bool
|
||||
hasIcon bool
|
||||
iconId tomo.Icon
|
||||
|
||||
onClick func ()
|
||||
}
|
||||
|
||||
// NewButton creates a new button with the specified label text.
|
||||
func NewButton (text string) (element *Button) {
|
||||
element = &Button { showText: true, enabled: true }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
buttonCase))
|
||||
element.SetText(text)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Button) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Button) Draw (destination art.Canvas) {
|
||||
state := element.state()
|
||||
bounds := element.entity.Bounds()
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, buttonCase)
|
||||
|
||||
pattern.Draw(destination, bounds)
|
||||
|
||||
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, buttonCase)
|
||||
sink := element.entity.Theme().Sink(tomo.PatternButton, buttonCase)
|
||||
margin := element.entity.Theme().Margin(tomo.PatternButton, buttonCase)
|
||||
|
||||
offset := image.Pt (
|
||||
bounds.Dx() / 2,
|
||||
bounds.Dy() / 2).Add(bounds.Min)
|
||||
|
||||
if element.showText {
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
offset.X -= textBounds.Dx() / 2
|
||||
offset.Y -= textBounds.Dy() / 2
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
}
|
||||
|
||||
if element.hasIcon {
|
||||
icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, buttonCase)
|
||||
if icon != nil {
|
||||
iconBounds := icon.Bounds()
|
||||
addedWidth := iconBounds.Dx()
|
||||
iconOffset := offset
|
||||
|
||||
if element.showText {
|
||||
addedWidth += margin.X
|
||||
}
|
||||
|
||||
iconOffset.X -= addedWidth / 2
|
||||
iconOffset.Y =
|
||||
bounds.Min.Y +
|
||||
(bounds.Dy() -
|
||||
iconBounds.Dy()) / 2
|
||||
if element.pressed {
|
||||
iconOffset = iconOffset.Add(sink)
|
||||
}
|
||||
offset.X += addedWidth / 2
|
||||
|
||||
icon.Draw(destination, foreground, iconOffset)
|
||||
}
|
||||
}
|
||||
|
||||
if element.showText {
|
||||
if element.pressed {
|
||||
offset = offset.Add(sink)
|
||||
}
|
||||
element.drawer.Draw(destination, foreground, offset)
|
||||
}
|
||||
}
|
||||
|
||||
// OnClick sets the function to be called when the button is clicked.
|
||||
func (element *Button) OnClick (callback func ()) {
|
||||
element.onClick = callback
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *Button) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this button is enabled or not.
|
||||
func (element *Button) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this button can be clicked or not.
|
||||
func (element *Button) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetText sets the button's label text.
|
||||
func (element *Button) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetIcon sets the icon of the button. Passing theme.IconNone removes the
|
||||
// current icon if it exists.
|
||||
func (element *Button) SetIcon (id tomo.Icon) {
|
||||
if id == tomo.IconNone {
|
||||
element.hasIcon = false
|
||||
} else {
|
||||
if element.hasIcon && element.iconId == id { return }
|
||||
element.hasIcon = true
|
||||
element.iconId = id
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// ShowText sets whether or not the button's text will be displayed.
|
||||
func (element *Button) ShowText (showText bool) {
|
||||
if element.showText == showText { return }
|
||||
element.showText = showText
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Button) HandleThemeChange () {
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
buttonCase))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Button) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Button) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Button) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = false
|
||||
within := position.In(element.entity.Bounds())
|
||||
if element.Enabled() && within && element.onClick != nil {
|
||||
element.onClick()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
if key == input.KeyEnter {
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
element.entity.Invalidate()
|
||||
if !element.Enabled() { return }
|
||||
if element.onClick != nil {
|
||||
element.onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Button) updateMinimumSize () {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternButton, buttonCase)
|
||||
margin := element.entity.Theme().Margin(tomo.PatternButton, buttonCase)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
minimumSize := textBounds.Sub(textBounds.Min)
|
||||
|
||||
if element.hasIcon {
|
||||
icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, buttonCase)
|
||||
if icon != nil {
|
||||
bounds := icon.Bounds()
|
||||
if element.showText {
|
||||
minimumSize.Max.X += bounds.Dx()
|
||||
minimumSize.Max.X += margin.X
|
||||
} else {
|
||||
minimumSize.Max.X = bounds.Dx()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
minimumSize = padding.Inverse().Apply(minimumSize)
|
||||
element.entity.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
|
||||
}
|
||||
|
||||
func (element *Button) state () tomo.State {
|
||||
return tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
Pressed: element.pressed,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
package elements
|
||||
|
||||
import "tomo"
|
||||
import "art"
|
||||
import "art/artutil"
|
||||
|
||||
var cellCase = tomo.C("tomo", "cell")
|
||||
|
||||
// Cell is a single-element container that satisfies tomo.Selectable. It
|
||||
// provides styling based on whether or not it is selected.
|
||||
type Cell struct {
|
||||
entity tomo.Entity
|
||||
child tomo.Element
|
||||
enabled bool
|
||||
|
||||
onSelectionChange func ()
|
||||
}
|
||||
|
||||
// NewCell creates a new cell element. If padding is true, the cell will have
|
||||
// padding on all sides. Child can be nil and added later with the Adopt()
|
||||
// method.
|
||||
func NewCell (child tomo.Element) (element *Cell) {
|
||||
element = &Cell { enabled: true }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.Adopt(child)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Cell) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Cell) Draw (destination art.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternTableCell, element.state(), cellCase)
|
||||
if element.child == nil {
|
||||
pattern.Draw(destination, bounds)
|
||||
} else {
|
||||
artutil.DrawShatter (
|
||||
destination, pattern, bounds,
|
||||
element.child.Entity().Bounds())
|
||||
}
|
||||
}
|
||||
|
||||
// Draw causes the element to perform a layout operation.
|
||||
func (element *Cell) Layout () {
|
||||
if element.child == nil { return }
|
||||
|
||||
bounds := element.entity.Bounds()
|
||||
bounds = element.entity.Theme().Padding(tomo.PatternTableCell, cellCase).Apply(bounds)
|
||||
|
||||
element.entity.PlaceChild(0, bounds)
|
||||
}
|
||||
|
||||
// DrawBackground draws this element's background pattern to the specified
|
||||
// destination canvas.
|
||||
func (element *Cell) DrawBackground (destination art.Canvas) {
|
||||
element.entity.Theme().Pattern(tomo.PatternTableCell, element.state(), cellCase).
|
||||
Draw(destination, element.entity.Bounds())
|
||||
}
|
||||
|
||||
// Adopt sets this element's child. If nil is passed, any child is removed.
|
||||
func (element *Cell) Adopt (child tomo.Element) {
|
||||
if element.child != nil {
|
||||
element.entity.Disown(element.entity.IndexOf(element.child))
|
||||
}
|
||||
if child != nil {
|
||||
element.entity.Adopt(child)
|
||||
}
|
||||
element.child = child
|
||||
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.invalidateChild()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// Child returns this element's child. If there is no child, this method will
|
||||
// return nil.
|
||||
func (element *Cell) Child () tomo.Element {
|
||||
return element.child
|
||||
}
|
||||
|
||||
// Enabled returns whether this cell is enabled or not.
|
||||
func (element *Cell) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this cell can be selected or not.
|
||||
func (element *Cell) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
element.invalidateChild()
|
||||
}
|
||||
|
||||
// OnSelectionChange sets a function to be called when this element is selected
|
||||
// or unselected.
|
||||
func (element *Cell) OnSelectionChange (callback func ()) {
|
||||
element.onSelectionChange = callback
|
||||
}
|
||||
|
||||
func (element *Cell) Selected () bool {
|
||||
return element.entity.Selected()
|
||||
}
|
||||
|
||||
func (element *Cell) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.invalidateChild()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *Cell) HandleSelectionChange () {
|
||||
element.entity.Invalidate()
|
||||
element.invalidateChild()
|
||||
if element.onSelectionChange != nil {
|
||||
element.onSelectionChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Cell) HandleChildMinimumSizeChange (tomo.Element) {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *Cell) state () tomo.State {
|
||||
return tomo.State {
|
||||
Disabled: !element.enabled,
|
||||
On: element.entity.Selected(),
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Cell) updateMinimumSize () {
|
||||
width, height := 0, 0
|
||||
|
||||
if element.child != nil {
|
||||
childWidth, childHeight := element.entity.ChildMinimumSize(0)
|
||||
width += childWidth
|
||||
height += childHeight
|
||||
}
|
||||
padding := element.entity.Theme().Padding(tomo.PatternTableCell, cellCase)
|
||||
width += padding.Horizontal()
|
||||
height += padding.Vertical()
|
||||
|
||||
element.entity.SetMinimumSize(width, height)
|
||||
}
|
||||
|
||||
func (element *Cell) invalidateChild () {
|
||||
if element.child != nil {
|
||||
element.child.Entity().Invalidate()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
import "tomo/textdraw"
|
||||
|
||||
var checkboxCase = tomo.C("tomo", "checkbox")
|
||||
|
||||
// Checkbox is a toggle-able checkbox with a label.
|
||||
type Checkbox struct {
|
||||
entity tomo.Entity
|
||||
drawer textdraw.Drawer
|
||||
|
||||
enabled bool
|
||||
pressed bool
|
||||
checked bool
|
||||
text string
|
||||
|
||||
onToggle func ()
|
||||
}
|
||||
|
||||
// NewCheckbox creates a new cbeckbox with the specified label text.
|
||||
func NewCheckbox (text string, checked bool) (element *Checkbox) {
|
||||
element = &Checkbox { checked: checked, enabled: true }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
checkboxCase))
|
||||
element.SetText(text)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Checkbox) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Checkbox) Draw (destination art.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
|
||||
|
||||
state := tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
Pressed: element.pressed,
|
||||
On: element.checked,
|
||||
}
|
||||
|
||||
element.entity.DrawBackground(destination)
|
||||
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, checkboxCase)
|
||||
pattern.Draw(destination, boxBounds)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
margin := element.entity.Theme().Margin(tomo.PatternBackground, checkboxCase)
|
||||
offset := bounds.Min.Add(image.Point {
|
||||
X: bounds.Dy() + margin.X,
|
||||
})
|
||||
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
|
||||
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, checkboxCase)
|
||||
element.drawer.Draw(destination, foreground, offset)
|
||||
}
|
||||
|
||||
// OnToggle sets the function to be called when the checkbox is toggled.
|
||||
func (element *Checkbox) OnToggle (callback func ()) {
|
||||
element.onToggle = callback
|
||||
}
|
||||
|
||||
// Value reports whether or not the checkbox is currently checked.
|
||||
func (element *Checkbox) Value () (checked bool) {
|
||||
return element.checked
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *Checkbox) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this checkbox is enabled or not.
|
||||
func (element *Checkbox) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this checkbox can be toggled or not.
|
||||
func (element *Checkbox) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetText sets the checkbox's label text.
|
||||
func (element *Checkbox) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleThemeChange () {
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
checkboxCase))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button != input.ButtonLeft || !element.pressed { return }
|
||||
|
||||
element.pressed = false
|
||||
within := position.In(element.entity.Bounds())
|
||||
if within {
|
||||
element.checked = !element.checked
|
||||
}
|
||||
|
||||
element.entity.Invalidate()
|
||||
if within && element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter {
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
element.checked = !element.checked
|
||||
element.entity.Invalidate()
|
||||
if element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Checkbox) updateMinimumSize () {
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
if element.text == "" {
|
||||
element.entity.SetMinimumSize(textBounds.Dy(), textBounds.Dy())
|
||||
} else {
|
||||
margin := element.entity.Theme().Margin(tomo.PatternBackground, checkboxCase)
|
||||
element.entity.SetMinimumSize (
|
||||
textBounds.Dy() + margin.X + textBounds.Dx(),
|
||||
textBounds.Dy())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,268 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
import "tomo/ability"
|
||||
import "tomo/textdraw"
|
||||
|
||||
var comboBoxCase = tomo.C("tomo", "comboBox")
|
||||
|
||||
// Option specifies a ComboBox option. A blank option will display as "(None)".
|
||||
type Option string
|
||||
|
||||
func (option Option) Title () string {
|
||||
if option == "" {
|
||||
return "(None)"
|
||||
} else {
|
||||
return string(option)
|
||||
}
|
||||
}
|
||||
|
||||
// ComboBox is an input that can be one of several predetermined values.
|
||||
type ComboBox struct {
|
||||
entity tomo.Entity
|
||||
drawer textdraw.Drawer
|
||||
|
||||
options []Option
|
||||
selected Option
|
||||
|
||||
enabled bool
|
||||
pressed bool
|
||||
|
||||
onChange func ()
|
||||
}
|
||||
|
||||
// NewComboBox creates a new ComboBox with the specifed options.
|
||||
func NewComboBox (options ...Option) (element *ComboBox) {
|
||||
if len(options) == 0 { options = []Option { "" } }
|
||||
element = &ComboBox { enabled: true, options: options }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
comboBoxCase))
|
||||
element.Select(options[0])
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *ComboBox) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *ComboBox) Draw (destination art.Canvas) {
|
||||
state := element.state()
|
||||
bounds := element.entity.Bounds()
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, comboBoxCase)
|
||||
|
||||
pattern.Draw(destination, bounds)
|
||||
|
||||
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, comboBoxCase)
|
||||
sink := element.entity.Theme().Sink(tomo.PatternButton, comboBoxCase)
|
||||
margin := element.entity.Theme().Margin(tomo.PatternButton, comboBoxCase)
|
||||
padding := element.entity.Theme().Padding(tomo.PatternButton, comboBoxCase)
|
||||
|
||||
offset := image.Pt(0, bounds.Dy() / 2).Add(bounds.Min)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
offset.Y -= textBounds.Dy() / 2
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
|
||||
icon := element.entity.Theme().Icon(tomo.IconExpand, tomo.IconSizeSmall, comboBoxCase)
|
||||
if icon != nil {
|
||||
iconBounds := icon.Bounds()
|
||||
addedWidth := iconBounds.Dx() + margin.X
|
||||
iconOffset := bounds.Min
|
||||
|
||||
iconOffset.X += padding[3]
|
||||
iconOffset.Y =
|
||||
bounds.Min.Y +
|
||||
(bounds.Dy() -
|
||||
iconBounds.Dy()) / 2
|
||||
if element.pressed {
|
||||
iconOffset = iconOffset.Add(sink)
|
||||
}
|
||||
offset.X += addedWidth + padding[3]
|
||||
|
||||
icon.Draw(destination, foreground, iconOffset)
|
||||
}
|
||||
|
||||
if element.pressed {
|
||||
offset = offset.Add(sink)
|
||||
}
|
||||
element.drawer.Draw(destination, foreground, offset)
|
||||
}
|
||||
|
||||
// OnChange sets the function to be called when this element's value is changed.
|
||||
func (element *ComboBox) OnChange (callback func ()) {
|
||||
element.onChange = callback
|
||||
}
|
||||
|
||||
// Value returns this element's value.
|
||||
func (element *ComboBox) Value () Option {
|
||||
return element.selected
|
||||
}
|
||||
|
||||
// Select sets this element's value.
|
||||
func (element *ComboBox) Select (option Option) {
|
||||
element.selected = option
|
||||
element.drawer.SetText([]rune(option.Title()))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
if element.onChange != nil {
|
||||
element.onChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Filled returns whether this element has a value other than (None).
|
||||
func (element *ComboBox) Filled () bool {
|
||||
return element.selected != ""
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *ComboBox) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this element is enabled or not.
|
||||
func (element *ComboBox) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this element is enabled or not.
|
||||
func (element *ComboBox) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ComboBox) HandleThemeChange () {
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
comboBoxCase))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ComboBox) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ComboBox) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
if button != input.ButtonLeft { return }
|
||||
element.dropDown()
|
||||
}
|
||||
|
||||
func (element *ComboBox) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) { }
|
||||
|
||||
func (element *ComboBox) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
|
||||
selectionDelta := 0
|
||||
switch key {
|
||||
case input.KeyEnter:
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
case input.KeyUp, input.KeyLeft:
|
||||
selectionDelta = -1
|
||||
case input.KeyDown, input.KeyRight:
|
||||
selectionDelta = 1
|
||||
}
|
||||
|
||||
if selectionDelta != 0 {
|
||||
selected := 0
|
||||
for index, option := range element.options {
|
||||
if option == element.selected {
|
||||
selected = index
|
||||
}
|
||||
}
|
||||
selected += selectionDelta
|
||||
if selected < 0 {
|
||||
selected = len(element.options) - 1
|
||||
} else if selected >= len(element.options) {
|
||||
selected = 0
|
||||
}
|
||||
|
||||
element.Select(element.options[selected])
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ComboBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
element.entity.Invalidate()
|
||||
if !element.Enabled() { return }
|
||||
element.dropDown()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ComboBox) dropDown () {
|
||||
window := element.entity.Window()
|
||||
menu, err := window.NewMenu(element.entity.Bounds())
|
||||
if err != nil { return }
|
||||
|
||||
cellToOption := make(map[ability.Selectable] Option)
|
||||
|
||||
list := NewList()
|
||||
for _, option := range element.options {
|
||||
option := option
|
||||
cell := NewCell(NewLabel(option.Title()))
|
||||
cellToOption[cell] = option
|
||||
list.Adopt(cell)
|
||||
|
||||
if option == element.selected {
|
||||
list.Select(cell)
|
||||
}
|
||||
}
|
||||
list.OnClick(func () {
|
||||
selected := list.Selected()
|
||||
if selected == nil { return }
|
||||
element.Select(cellToOption[selected])
|
||||
menu.Close()
|
||||
})
|
||||
|
||||
menu.Adopt(list)
|
||||
list.Focus()
|
||||
menu.Show()
|
||||
}
|
||||
|
||||
func (element *ComboBox) updateMinimumSize () {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternButton, comboBoxCase)
|
||||
margin := element.entity.Theme().Margin(tomo.PatternButton, comboBoxCase)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
minimumSize := textBounds.Sub(textBounds.Min)
|
||||
|
||||
icon := element.entity.Theme().Icon(tomo.IconExpand, tomo.IconSizeSmall, comboBoxCase)
|
||||
if icon != nil {
|
||||
bounds := icon.Bounds()
|
||||
minimumSize.Max.X += bounds.Dx()
|
||||
minimumSize.Max.X += margin.X
|
||||
}
|
||||
|
||||
minimumSize = padding.Inverse().Apply(minimumSize)
|
||||
element.entity.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
|
||||
}
|
||||
|
||||
func (element *ComboBox) state () tomo.State {
|
||||
return tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
Pressed: element.pressed,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package elements
|
||||
|
||||
import "tomo"
|
||||
|
||||
type scratchEntry struct {
|
||||
expand bool
|
||||
minSize float64
|
||||
minBreadth float64
|
||||
}
|
||||
|
||||
type container struct {
|
||||
entity tomo.Entity
|
||||
scratch map[tomo.Element] scratchEntry
|
||||
minimumSize func ()
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (container *container) Entity () tomo.Entity {
|
||||
return container.entity
|
||||
}
|
||||
|
||||
// Adopt adds one or more elements to the container.
|
||||
func (container *container) Adopt (children ...tomo.Element) {
|
||||
container.adopt(false, children...)
|
||||
}
|
||||
|
||||
func (container *container) init () {
|
||||
container.scratch = make(map[tomo.Element] scratchEntry)
|
||||
}
|
||||
|
||||
func (container *container) adopt (expand bool, children ...tomo.Element) {
|
||||
for _, child := range children {
|
||||
container.entity.Adopt(child)
|
||||
container.scratch[child] = scratchEntry { expand: expand }
|
||||
}
|
||||
container.minimumSize()
|
||||
container.entity.Invalidate()
|
||||
container.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// Disown removes one or more elements from the container.
|
||||
func (container *container) Disown (children ...tomo.Element) {
|
||||
for _, child := range children {
|
||||
index := container.entity.IndexOf(child)
|
||||
if index < 0 { continue }
|
||||
container.entity.Disown(index)
|
||||
delete(container.scratch, child)
|
||||
}
|
||||
container.minimumSize()
|
||||
container.entity.Invalidate()
|
||||
container.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// DisownAll removes all elements from the container.
|
||||
func (container *container) DisownAll () {
|
||||
func () {
|
||||
for index := 0; index < container.entity.CountChildren(); index ++ {
|
||||
index := index
|
||||
defer container.entity.Disown(index)
|
||||
}
|
||||
} ()
|
||||
container.scratch = make(map[tomo.Element] scratchEntry)
|
||||
container.minimumSize()
|
||||
container.entity.Invalidate()
|
||||
container.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// Child returns the child at the specified index.
|
||||
func (container *container) Child (index int) tomo.Element {
|
||||
if index < 0 || index >= container.entity.CountChildren() { return nil }
|
||||
return container.entity.Child(index)
|
||||
}
|
||||
|
||||
// CountChildren returns the amount of children in this container.
|
||||
func (container *container) CountChildren () int {
|
||||
return container.entity.CountChildren()
|
||||
}
|
||||
|
||||
func (container *container) HandleChildMinimumSizeChange (child tomo.Element) {
|
||||
container.minimumSize()
|
||||
container.entity.Invalidate()
|
||||
container.entity.InvalidateLayout()
|
||||
}
|
|
@ -1,149 +0,0 @@
|
|||
package core
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
// Core is a struct that implements some core functionality common to most
|
||||
// widgets. It is meant to be embedded directly into a struct.
|
||||
type Core struct {
|
||||
canvas tomo.Canvas
|
||||
|
||||
metrics struct {
|
||||
minimumWidth int
|
||||
minimumHeight int
|
||||
}
|
||||
|
||||
drawSizeChange func ()
|
||||
onMinimumSizeChange func ()
|
||||
onDamage func (region tomo.Canvas)
|
||||
}
|
||||
|
||||
// NewCore creates a new element core and its corresponding control.
|
||||
func NewCore (drawSizeChange func ()) (core *Core, control CoreControl) {
|
||||
core = &Core { drawSizeChange: drawSizeChange }
|
||||
control = CoreControl { core: core }
|
||||
return
|
||||
}
|
||||
|
||||
// ColorModel fulfills the draw.Image interface.
|
||||
func (core *Core) ColorModel () (model color.Model) {
|
||||
return color.RGBAModel
|
||||
}
|
||||
|
||||
// ColorModel fulfills the draw.Image interface.
|
||||
func (core *Core) At (x, y int) (pixel color.Color) {
|
||||
if core.canvas == nil { return }
|
||||
return core.canvas.At(x, y)
|
||||
}
|
||||
|
||||
// ColorModel fulfills the draw.Image interface.
|
||||
func (core *Core) Bounds () (bounds image.Rectangle) {
|
||||
if core.canvas == nil { return }
|
||||
return core.canvas.Bounds()
|
||||
}
|
||||
|
||||
// ColorModel fulfills the draw.Image interface.
|
||||
func (core *Core) Set (x, y int, c color.Color) () {
|
||||
if core.canvas == nil { return }
|
||||
core.canvas.Set(x, y, c)
|
||||
}
|
||||
|
||||
// Buffer fulfills the tomo.Canvas interface.
|
||||
func (core *Core) Buffer () (data []color.RGBA, stride int) {
|
||||
if core.canvas == nil { return }
|
||||
return core.canvas.Buffer()
|
||||
}
|
||||
|
||||
// MinimumSize fulfils the tomo.Element interface. This should not need to be
|
||||
// overridden.
|
||||
func (core *Core) MinimumSize () (width, height int) {
|
||||
return core.metrics.minimumWidth, core.metrics.minimumHeight
|
||||
}
|
||||
|
||||
// DrawTo fulfills the tomo.Element interface. This should not need to be
|
||||
// overridden.
|
||||
func (core *Core) DrawTo (canvas tomo.Canvas) {
|
||||
core.canvas = canvas
|
||||
if core.drawSizeChange != nil {
|
||||
core.drawSizeChange()
|
||||
}
|
||||
}
|
||||
|
||||
// OnDamage fulfils the tomo.Element interface. This should not need to be
|
||||
// overridden.
|
||||
func (core *Core) OnDamage (callback func (region tomo.Canvas)) {
|
||||
core.onDamage = callback
|
||||
}
|
||||
|
||||
// OnMinimumSizeChange fulfils the tomo.Element interface. This should not need
|
||||
// to be overridden.
|
||||
func (core *Core) OnMinimumSizeChange (callback func ()) {
|
||||
core.onMinimumSizeChange = callback
|
||||
}
|
||||
|
||||
// CoreControl is a struct that can exert control over a Core struct. It can be
|
||||
// used as a canvas. It must not be directly embedded into an element, but
|
||||
// instead kept as a private member. When a Core struct is created, a
|
||||
// corresponding CoreControl struct is linked to it and returned alongside it.
|
||||
type CoreControl struct {
|
||||
core *Core
|
||||
}
|
||||
|
||||
// HasImage returns true if the core has an allocated image buffer, and false if
|
||||
// it doesn't.
|
||||
func (control CoreControl) HasImage () (has bool) {
|
||||
return control.core.canvas != nil && !control.core.canvas.Bounds().Empty()
|
||||
}
|
||||
|
||||
// DamageRegion pushes the selected region of pixels to the parent element. This
|
||||
// does not need to be called when responding to a resize event.
|
||||
func (control CoreControl) DamageRegion (bounds image.Rectangle) {
|
||||
if control.core.onDamage != nil {
|
||||
control.core.onDamage(tomo.Cut(control.core, bounds))
|
||||
}
|
||||
}
|
||||
|
||||
// DamageAll pushes all pixels to the parent element. This does not need to be
|
||||
// called when redrawing in response to a change in size.
|
||||
func (control CoreControl) DamageAll () {
|
||||
control.DamageRegion(control.core.Bounds())
|
||||
}
|
||||
|
||||
// SetMinimumSize sets the minimum size of this element, notifying the parent
|
||||
// element in the process.
|
||||
func (control CoreControl) SetMinimumSize (width, height int) {
|
||||
core := control.core
|
||||
if width == core.metrics.minimumWidth &&
|
||||
height == core.metrics.minimumHeight {
|
||||
return
|
||||
}
|
||||
|
||||
core.metrics.minimumWidth = width
|
||||
core.metrics.minimumHeight = height
|
||||
if control.core.onMinimumSizeChange != nil {
|
||||
control.core.onMinimumSizeChange()
|
||||
}
|
||||
}
|
||||
|
||||
// ConstrainSize contstrains the specified width and height to the minimum width
|
||||
// and height, and returns wether or not anything ended up being constrained.
|
||||
func (control CoreControl) ConstrainSize (
|
||||
inWidth, inHeight int,
|
||||
) (
|
||||
outWidth, outHeight int,
|
||||
constrained bool,
|
||||
) {
|
||||
core := control.core
|
||||
outWidth = inWidth
|
||||
outHeight = inHeight
|
||||
if outWidth < core.metrics.minimumWidth {
|
||||
outWidth = core.metrics.minimumWidth
|
||||
constrained = true
|
||||
}
|
||||
if outHeight < core.metrics.minimumHeight {
|
||||
outHeight = core.metrics.minimumHeight
|
||||
constrained = true
|
||||
}
|
||||
return
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
package core
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
// FocusableCore is a struct that can be embedded into objects to make them
|
||||
// focusable, giving them the default keynav behavior.
|
||||
type FocusableCore struct {
|
||||
focused bool
|
||||
enabled bool
|
||||
drawFocusChange func ()
|
||||
onFocusRequest func () (granted bool)
|
||||
onFocusMotionRequest func(tomo.KeynavDirection) (granted bool)
|
||||
}
|
||||
|
||||
// NewFocusableCore creates a new focusability core and its corresponding
|
||||
// control. If your element needs to visually update itself when it's focus
|
||||
// state changes (which it should), a callback to draw and push the update can
|
||||
// be specified.
|
||||
func NewFocusableCore (
|
||||
drawFocusChange func (),
|
||||
) (
|
||||
core *FocusableCore,
|
||||
control FocusableCoreControl,
|
||||
) {
|
||||
core = &FocusableCore {
|
||||
drawFocusChange: drawFocusChange,
|
||||
enabled: true,
|
||||
}
|
||||
control = FocusableCoreControl { core: core }
|
||||
return
|
||||
}
|
||||
|
||||
// Focused returns whether or not this element is currently focused.
|
||||
func (core *FocusableCore) Focused () (focused bool) {
|
||||
return core.focused
|
||||
}
|
||||
|
||||
// Focus focuses this element, if its parent element grants the request.
|
||||
func (core *FocusableCore) Focus () {
|
||||
if !core.enabled { return }
|
||||
if core.onFocusRequest != nil {
|
||||
core.onFocusRequest()
|
||||
}
|
||||
}
|
||||
|
||||
// HandleFocus causes this element to mark itself as focused, if it can
|
||||
// currently be. Otherwise, it will return false and do nothing.
|
||||
func (core *FocusableCore) HandleFocus (
|
||||
direction tomo.KeynavDirection,
|
||||
) (
|
||||
accepted bool,
|
||||
) {
|
||||
direction = direction.Canon()
|
||||
if !core.enabled { return false }
|
||||
if core.focused && direction != tomo.KeynavDirectionNeutral {
|
||||
return false
|
||||
}
|
||||
|
||||
core.focused = true
|
||||
if core.drawFocusChange != nil { core.drawFocusChange() }
|
||||
return true
|
||||
}
|
||||
|
||||
// HandleUnfocus causes this element to mark itself as unfocused.
|
||||
func (core *FocusableCore) HandleUnfocus () {
|
||||
core.focused = false
|
||||
if core.drawFocusChange != nil { core.drawFocusChange() }
|
||||
}
|
||||
|
||||
// OnFocusRequest sets a function to be called when this element
|
||||
// wants its parent element to focus it. Parent elements should return
|
||||
// true if the request was granted, and false if it was not.
|
||||
func (core *FocusableCore) OnFocusRequest (callback func () (granted bool)) {
|
||||
core.onFocusRequest = callback
|
||||
}
|
||||
|
||||
// OnFocusMotionRequest sets a function to be called when this
|
||||
// element wants its parent element to focus the element behind or in
|
||||
// front of it, depending on the specified direction. Parent elements
|
||||
// should return true if the request was granted, and false if it was
|
||||
// not.
|
||||
func (core *FocusableCore) OnFocusMotionRequest (
|
||||
callback func (direction tomo.KeynavDirection) (granted bool),
|
||||
) {
|
||||
core.onFocusMotionRequest = callback
|
||||
}
|
||||
|
||||
// Enabled returns whether or not the element is enabled.
|
||||
func (core *FocusableCore) Enabled () (enabled bool) {
|
||||
return core.enabled
|
||||
}
|
||||
|
||||
// FocusableCoreControl is a struct that can be used to exert control over a
|
||||
// focusability core. It must not be directly embedded into an element, but
|
||||
// instead kept as a private member. When a FocusableCore struct is created, a
|
||||
// corresponding FocusableCoreControl struct is linked to it and returned
|
||||
// alongside it.
|
||||
type FocusableCoreControl struct {
|
||||
core *FocusableCore
|
||||
}
|
||||
|
||||
// SetEnabled sets whether the focusability core is enabled. If the state
|
||||
// changes, this will call drawFocusChange.
|
||||
func (control FocusableCoreControl) SetEnabled (enabled bool) {
|
||||
if control.core.enabled == enabled { return }
|
||||
control.core.enabled = enabled
|
||||
if !enabled { control.core.focused = false }
|
||||
if control.core.drawFocusChange != nil {
|
||||
control.core.drawFocusChange()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,322 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "path/filepath"
|
||||
import "tomo"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
import "tomo/ability"
|
||||
import "art/shatter"
|
||||
|
||||
// TODO: base on flow implementation of list. also be able to switch to a table
|
||||
// variant for a more information dense view.
|
||||
|
||||
var directoryCase = tomo.C("tomo", "list")
|
||||
|
||||
type historyEntry struct {
|
||||
location string
|
||||
filesystem ReadDirStatFS
|
||||
}
|
||||
|
||||
// Directory displays a list of files within a particular directory and
|
||||
// file system.
|
||||
type Directory struct {
|
||||
container
|
||||
entity tomo.Entity
|
||||
|
||||
scroll image.Point
|
||||
contentBounds image.Rectangle
|
||||
|
||||
history []historyEntry
|
||||
historyIndex int
|
||||
|
||||
onChoose func (file string)
|
||||
onScrollBoundsChange func ()
|
||||
}
|
||||
|
||||
// NewDirectory creates a new directory view. If within is nil, it will use
|
||||
// the OS file system.
|
||||
func NewDirectory (
|
||||
location string,
|
||||
within ReadDirStatFS,
|
||||
) (
|
||||
element *Directory,
|
||||
err error,
|
||||
) {
|
||||
element = &Directory { }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.container.entity = element.entity
|
||||
element.minimumSize = element.updateMinimumSize
|
||||
element.init()
|
||||
err = element.SetLocation(location, within)
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Directory) Draw (destination art.Canvas) {
|
||||
rocks := make([]image.Rectangle, element.entity.CountChildren())
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
rocks[index] = element.entity.Child(index).Entity().Bounds()
|
||||
}
|
||||
|
||||
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
|
||||
for _, tile := range tiles {
|
||||
element.DrawBackground(art.Cut(destination, tile))
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Directory) Layout () {
|
||||
if element.scroll.Y > element.maxScrollHeight() {
|
||||
element.scroll.Y = element.maxScrollHeight()
|
||||
}
|
||||
|
||||
margin := element.entity.Theme().Margin(tomo.PatternPinboard, directoryCase)
|
||||
padding := element.entity.Theme().Padding(tomo.PatternPinboard, directoryCase)
|
||||
bounds := padding.Apply(element.entity.Bounds())
|
||||
element.contentBounds = image.Rectangle { }
|
||||
|
||||
dot := bounds.Min.Sub(element.scroll)
|
||||
xStart := dot.X
|
||||
rowHeight := 0
|
||||
|
||||
nextLine := func () {
|
||||
dot.X = xStart
|
||||
dot.Y += margin.Y
|
||||
dot.Y += rowHeight
|
||||
rowHeight = 0
|
||||
}
|
||||
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
child := element.entity.Child(index)
|
||||
entry := element.scratch[child]
|
||||
|
||||
width := int(entry.minBreadth)
|
||||
height := int(entry.minSize)
|
||||
if width + dot.X > bounds.Max.X {
|
||||
nextLine()
|
||||
}
|
||||
if typedChild, ok := child.(ability.Flexible); ok {
|
||||
height = typedChild.FlexibleHeightFor(width)
|
||||
}
|
||||
if rowHeight < height {
|
||||
rowHeight = height
|
||||
}
|
||||
|
||||
childBounds := tomo.Bounds (
|
||||
dot.X, dot.Y,
|
||||
width, height)
|
||||
element.entity.PlaceChild(index, childBounds)
|
||||
element.contentBounds = element.contentBounds.Union(childBounds)
|
||||
|
||||
dot.X += width + margin.X
|
||||
}
|
||||
|
||||
element.contentBounds =
|
||||
element.contentBounds.Sub(element.contentBounds.Min)
|
||||
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Directory) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
element.selectNone()
|
||||
}
|
||||
|
||||
func (element *Directory) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) { }
|
||||
|
||||
func (element *Directory) HandleChildMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
child tomo.Element,
|
||||
) {
|
||||
element.selectNone()
|
||||
if child, ok := child.(ability.Selectable); ok {
|
||||
index := element.entity.IndexOf(child)
|
||||
element.entity.SelectChild(index, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Directory) HandleChildMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
child tomo.Element,
|
||||
) { }
|
||||
|
||||
func (element *Directory) HandleChildFlexibleHeightChange (child ability.Flexible) {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// ScrollContentBounds returns the full content size of the element.
|
||||
func (element *Directory) ScrollContentBounds () image.Rectangle {
|
||||
return element.contentBounds
|
||||
}
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's
|
||||
// viewport relative to ScrollBounds.
|
||||
func (element *Directory) ScrollViewportBounds () image.Rectangle {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternPinboard, directoryCase)
|
||||
bounds := padding.Apply(element.entity.Bounds())
|
||||
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
|
||||
return bounds
|
||||
}
|
||||
|
||||
// ScrollTo scrolls the viewport to the specified point relative to
|
||||
// ScrollBounds.
|
||||
func (element *Directory) ScrollTo (position image.Point) {
|
||||
if position.Y < 0 {
|
||||
position.Y = 0
|
||||
}
|
||||
maxScrollHeight := element.maxScrollHeight()
|
||||
if position.Y > maxScrollHeight {
|
||||
position.Y = maxScrollHeight
|
||||
}
|
||||
element.scroll = position
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// OnScrollBoundsChange sets a function to be called when the element's viewport
|
||||
// bounds, content bounds, or scroll axes change.
|
||||
func (element *Directory) OnScrollBoundsChange (callback func ()) {
|
||||
element.onScrollBoundsChange = callback
|
||||
}
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
func (element *Directory) ScrollAxes () (horizontal, vertical bool) {
|
||||
return false, true
|
||||
}
|
||||
|
||||
func (element *Directory) DrawBackground (destination art.Canvas) {
|
||||
element.entity.Theme().Pattern(tomo.PatternPinboard, tomo.State { }, directoryCase).
|
||||
Draw(destination, element.entity.Bounds())
|
||||
}
|
||||
|
||||
func (element *Directory) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// Location returns the directory's location and filesystem.
|
||||
func (element *Directory) Location () (string, ReadDirStatFS) {
|
||||
if len(element.history) < 1 { return "", nil }
|
||||
current := element.history[element.historyIndex]
|
||||
return current.location, current.filesystem
|
||||
}
|
||||
|
||||
// SetLocation sets the directory's location and filesystem. If within is nil,
|
||||
// it will use the OS file system.
|
||||
func (element *Directory) SetLocation (
|
||||
location string,
|
||||
within ReadDirStatFS,
|
||||
) error {
|
||||
if within == nil {
|
||||
within = defaultFS { }
|
||||
}
|
||||
element.scroll = image.Point { }
|
||||
|
||||
if element.history != nil {
|
||||
element.historyIndex ++
|
||||
}
|
||||
element.history = append (
|
||||
element.history[:element.historyIndex],
|
||||
historyEntry { location, within })
|
||||
return element.Update()
|
||||
}
|
||||
|
||||
// Backward goes back a directory in history
|
||||
func (element *Directory) Backward () (bool, error) {
|
||||
if element.historyIndex > 1 {
|
||||
element.historyIndex --
|
||||
return true, element.Update()
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Forward goes forward a directory in history
|
||||
func (element *Directory) Forward () (bool, error) {
|
||||
if element.historyIndex < len(element.history) - 1 {
|
||||
element.historyIndex ++
|
||||
return true, element.Update()
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Update refreshes the directory's contents.
|
||||
func (element *Directory) Update () error {
|
||||
location, filesystem := element.Location()
|
||||
entries, err := filesystem.ReadDir(location)
|
||||
|
||||
children := make([]tomo.Element, len(entries))
|
||||
for index, entry := range entries {
|
||||
filePath := filepath.Join(location, entry.Name())
|
||||
file, _ := NewFile(filePath, filesystem)
|
||||
file.OnChoose (func () {
|
||||
if element.onChoose != nil {
|
||||
element.onChoose(filePath)
|
||||
}
|
||||
})
|
||||
|
||||
children[index] = file
|
||||
}
|
||||
|
||||
element.DisownAll()
|
||||
element.Adopt(children...)
|
||||
return err
|
||||
}
|
||||
|
||||
// OnChoose sets a function to be called when the user double-clicks a file or
|
||||
// sub-directory within the directory view.
|
||||
func (element *Directory) OnChoose (callback func (file string)) {
|
||||
element.onChoose = callback
|
||||
}
|
||||
|
||||
func (element *Directory) selectNone () {
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
element.entity.SelectChild(index, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Directory) maxScrollHeight () (height int) {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternSunken, directoryCase)
|
||||
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
|
||||
height = element.contentBounds.Dy() - viewportHeight
|
||||
if height < 0 { height = 0 }
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
func (element *Directory) updateMinimumSize () {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternPinboard, directoryCase)
|
||||
minimumWidth := 0
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
width, height := element.entity.ChildMinimumSize(index)
|
||||
if width > minimumWidth {
|
||||
minimumWidth = width
|
||||
}
|
||||
|
||||
key := element.entity.Child(index)
|
||||
entry := element.scratch[key]
|
||||
entry.minSize = float64(height)
|
||||
entry.minBreadth = float64(width)
|
||||
element.scratch[key] = entry
|
||||
}
|
||||
element.entity.SetMinimumSize (
|
||||
minimumWidth + padding.Horizontal(),
|
||||
padding.Vertical())
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// Package elements provides standard elements that are commonly used in GUI
|
||||
// applications.
|
||||
package elements
|
|
@ -0,0 +1,211 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "art"
|
||||
import "tomo/ability"
|
||||
import "art/shatter"
|
||||
|
||||
var documentCase = tomo.C("tomo", "document")
|
||||
|
||||
// Document is a scrollable container capcable of laying out flexible child
|
||||
// elements. Children can be added either inline (similar to an HTML/CSS inline
|
||||
// element), or expanding (similar to an HTML/CSS block element).
|
||||
type Document struct {
|
||||
container
|
||||
entity tomo.Entity
|
||||
|
||||
scroll image.Point
|
||||
contentBounds image.Rectangle
|
||||
|
||||
onScrollBoundsChange func ()
|
||||
}
|
||||
|
||||
// NewDocument creates a new document container.
|
||||
func NewDocument (children ...tomo.Element) (element *Document) {
|
||||
element = &Document { }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.container.entity = element.entity
|
||||
element.minimumSize = element.updateMinimumSize
|
||||
element.init()
|
||||
element.Adopt(children...)
|
||||
return
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Document) Draw (destination art.Canvas) {
|
||||
rocks := make([]image.Rectangle, element.entity.CountChildren())
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
rocks[index] = element.entity.Child(index).Entity().Bounds()
|
||||
}
|
||||
|
||||
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
|
||||
for _, tile := range tiles {
|
||||
element.entity.DrawBackground(art.Cut(destination, tile))
|
||||
}
|
||||
}
|
||||
|
||||
// Layout causes this element to perform a layout operation.
|
||||
func (element *Document) Layout () {
|
||||
if element.scroll.Y > element.maxScrollHeight() {
|
||||
element.scroll.Y = element.maxScrollHeight()
|
||||
}
|
||||
|
||||
margin := element.entity.Theme().Margin(tomo.PatternBackground, documentCase)
|
||||
padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
|
||||
bounds := padding.Apply(element.entity.Bounds())
|
||||
element.contentBounds = image.Rectangle { }
|
||||
|
||||
dot := bounds.Min.Sub(element.scroll)
|
||||
xStart := dot.X
|
||||
rowHeight := 0
|
||||
|
||||
nextLine := func () {
|
||||
dot.X = xStart
|
||||
dot.Y += margin.Y
|
||||
dot.Y += rowHeight
|
||||
rowHeight = 0
|
||||
}
|
||||
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
child := element.entity.Child(index)
|
||||
entry := element.scratch[child]
|
||||
|
||||
if dot.X > xStart && entry.expand {
|
||||
nextLine()
|
||||
}
|
||||
|
||||
width := int(entry.minBreadth)
|
||||
height := int(entry.minSize)
|
||||
if width + dot.X > bounds.Max.X && !entry.expand {
|
||||
nextLine()
|
||||
}
|
||||
if width < bounds.Dx() && entry.expand {
|
||||
width = bounds.Dx()
|
||||
}
|
||||
if typedChild, ok := child.(ability.Flexible); ok {
|
||||
height = typedChild.FlexibleHeightFor(width)
|
||||
}
|
||||
if rowHeight < height {
|
||||
rowHeight = height
|
||||
}
|
||||
|
||||
childBounds := tomo.Bounds (
|
||||
dot.X, dot.Y,
|
||||
width, height)
|
||||
element.entity.PlaceChild(index, childBounds)
|
||||
element.contentBounds = element.contentBounds.Union(childBounds)
|
||||
|
||||
if entry.expand {
|
||||
nextLine()
|
||||
} else {
|
||||
dot.X += width + margin.X
|
||||
}
|
||||
}
|
||||
|
||||
element.contentBounds =
|
||||
element.contentBounds.Sub(element.contentBounds.Min)
|
||||
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Adopt adds one or more elements to the container, placing each on its own
|
||||
// line.
|
||||
func (element *Document) Adopt (children ...tomo.Element) {
|
||||
element.adopt(true, children...)
|
||||
}
|
||||
|
||||
// AdoptInline adds one or more elements to the container, packing multiple
|
||||
// elements onto the same line(s).
|
||||
func (element *Document) AdoptInline (children ...tomo.Element) {
|
||||
element.adopt(false, children...)
|
||||
}
|
||||
|
||||
func (element *Document) HandleChildFlexibleHeightChange (child ability.Flexible) {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// DrawBackground draws this element's background pattern to the specified
|
||||
// destination canvas.
|
||||
func (element *Document) DrawBackground (destination art.Canvas) {
|
||||
element.entity.DrawBackground(destination)
|
||||
}
|
||||
|
||||
func (element *Document) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// ScrollContentBounds returns the full content size of the element.
|
||||
func (element *Document) ScrollContentBounds () image.Rectangle {
|
||||
return element.contentBounds
|
||||
}
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's
|
||||
// viewport relative to ScrollBounds.
|
||||
func (element *Document) ScrollViewportBounds () image.Rectangle {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
|
||||
bounds := padding.Apply(element.entity.Bounds())
|
||||
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
|
||||
return bounds
|
||||
}
|
||||
|
||||
// ScrollTo scrolls the viewport to the specified point relative to
|
||||
// ScrollBounds.
|
||||
func (element *Document) ScrollTo (position image.Point) {
|
||||
if position.Y < 0 {
|
||||
position.Y = 0
|
||||
}
|
||||
maxScrollHeight := element.maxScrollHeight()
|
||||
if position.Y > maxScrollHeight {
|
||||
position.Y = maxScrollHeight
|
||||
}
|
||||
element.scroll = position
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// OnScrollBoundsChange sets a function to be called when the element's viewport
|
||||
// bounds, content bounds, or scroll axes change.
|
||||
func (element *Document) OnScrollBoundsChange (callback func ()) {
|
||||
element.onScrollBoundsChange = callback
|
||||
}
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
func (element *Document) ScrollAxes () (horizontal, vertical bool) {
|
||||
return false, true
|
||||
}
|
||||
|
||||
func (element *Document) maxScrollHeight () (height int) {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
|
||||
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
|
||||
height = element.contentBounds.Dy() - viewportHeight
|
||||
if height < 0 { height = 0 }
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Document) updateMinimumSize () {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
|
||||
minimumWidth := 0
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
width, height := element.entity.ChildMinimumSize(index)
|
||||
if width > minimumWidth {
|
||||
minimumWidth = width
|
||||
}
|
||||
|
||||
key := element.entity.Child(index)
|
||||
entry := element.scratch[key]
|
||||
entry.minSize = float64(height)
|
||||
entry.minBreadth = float64(width)
|
||||
element.scratch[key] = entry
|
||||
}
|
||||
element.entity.SetMinimumSize (
|
||||
minimumWidth + padding.Horizontal(),
|
||||
padding.Vertical())
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
package elements
|
||||
|
||||
import "time"
|
||||
import "io/fs"
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
|
||||
var fileCase = tomo.C("files", "file")
|
||||
|
||||
// File displays an interactive visual representation of a file within any
|
||||
// file system.
|
||||
type File struct {
|
||||
entity tomo.Entity
|
||||
|
||||
lastClick time.Time
|
||||
pressed bool
|
||||
enabled bool
|
||||
iconID tomo.Icon
|
||||
filesystem fs.StatFS
|
||||
location string
|
||||
|
||||
onChoose func ()
|
||||
}
|
||||
|
||||
// NewFile creates a new file element. If within is nil, it will use the OS file
|
||||
// system
|
||||
func NewFile (
|
||||
location string,
|
||||
within fs.StatFS,
|
||||
) (
|
||||
element *File,
|
||||
err error,
|
||||
) {
|
||||
element = &File { enabled: true }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
err = element.SetLocation(location, within)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *File) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *File) Draw (destination art.Canvas) {
|
||||
// background
|
||||
state := element.state()
|
||||
bounds := element.entity.Bounds()
|
||||
sink := element.entity.Theme().Sink(tomo.PatternButton, fileCase)
|
||||
element.entity.Theme().
|
||||
Pattern(tomo.PatternButton, state, fileCase).
|
||||
Draw(destination, bounds)
|
||||
|
||||
// icon
|
||||
icon := element.icon()
|
||||
if icon != nil {
|
||||
iconBounds := icon.Bounds()
|
||||
offset := image.Pt (
|
||||
(bounds.Dx() - iconBounds.Dx()) / 2,
|
||||
(bounds.Dy() - iconBounds.Dy()) / 2)
|
||||
if element.pressed {
|
||||
offset = offset.Add(sink)
|
||||
}
|
||||
icon.Draw (
|
||||
destination,
|
||||
element.entity.Theme().Color(tomo.ColorForeground, state, fileCase),
|
||||
bounds.Min.Add(offset))
|
||||
}
|
||||
}
|
||||
// Location returns the file's location and filesystem.
|
||||
func (element *File) Location () (string, fs.StatFS) {
|
||||
return element.location, element.filesystem
|
||||
}
|
||||
|
||||
// SetLocation sets the file's location and filesystem. If within is nil, it
|
||||
// will use the OS file system.
|
||||
func (element *File) SetLocation (
|
||||
location string,
|
||||
within fs.StatFS,
|
||||
) error {
|
||||
if within == nil {
|
||||
within = defaultFS { }
|
||||
}
|
||||
element.location = location
|
||||
element.filesystem = within
|
||||
return element.Update()
|
||||
}
|
||||
|
||||
// Update refreshes the element to match the file it represents.
|
||||
func (element *File) Update () error {
|
||||
info, err := element.filesystem.Stat(element.location)
|
||||
|
||||
if err != nil {
|
||||
element.iconID = tomo.IconError
|
||||
} else if info.IsDir() {
|
||||
element.iconID = tomo.IconDirectory
|
||||
} else {
|
||||
// TODO: choose icon based on file mime type
|
||||
element.iconID = tomo.IconFile
|
||||
}
|
||||
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
return err
|
||||
}
|
||||
|
||||
func (element *File) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
if key == input.KeyEnter {
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *File) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
if !element.Enabled() { return }
|
||||
element.entity.Invalidate()
|
||||
if element.onChoose != nil {
|
||||
element.onChoose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *File) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *File) HandleSelectionChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *File) OnChoose (callback func ()) {
|
||||
element.onChoose = callback
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *File) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this file is enabled or not.
|
||||
func (element *File) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this file is enabled or not.
|
||||
func (element *File) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *File) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
if !element.entity.Focused() { element.Focus() }
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *File) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = false
|
||||
within := position.In(element.entity.Bounds())
|
||||
if time.Since(element.lastClick) < element.entity.Config().DoubleClickDelay() {
|
||||
if element.Enabled() && within && element.onChoose != nil {
|
||||
element.onChoose()
|
||||
}
|
||||
} else {
|
||||
element.lastClick = time.Now()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *File) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *File) state () tomo.State {
|
||||
return tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
Pressed: element.pressed,
|
||||
On: element.entity.Selected(),
|
||||
}
|
||||
}
|
||||
|
||||
func (element *File) icon () art.Icon {
|
||||
return element.entity.Theme().Icon(element.iconID, tomo.IconSizeLarge, fileCase)
|
||||
}
|
||||
|
||||
func (element *File) updateMinimumSize () {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternButton, fileCase)
|
||||
icon := element.icon()
|
||||
if icon == nil {
|
||||
element.entity.SetMinimumSize (
|
||||
padding.Horizontal(),
|
||||
padding.Vertical())
|
||||
} else {
|
||||
bounds := padding.Inverse().Apply(icon.Bounds())
|
||||
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package elements
|
||||
|
||||
import "os"
|
||||
import "io/fs"
|
||||
|
||||
// ReadDirStatFS is a combination of fs.ReadDirFS and fs.StatFS. It is the
|
||||
// minimum filesystem needed to satisfy a directory view.
|
||||
type ReadDirStatFS interface {
|
||||
fs.ReadDirFS
|
||||
fs.StatFS
|
||||
}
|
||||
|
||||
type defaultFS struct { }
|
||||
|
||||
func (defaultFS) Open (name string) (fs.File, error) {
|
||||
return os.Open(name)
|
||||
}
|
||||
|
||||
func (defaultFS) ReadDir (name string) ([]fs.DirEntry, error) {
|
||||
return os.ReadDir(name)
|
||||
}
|
||||
|
||||
func (defaultFS) Stat (name string) (fs.FileInfo, error) {
|
||||
return os.Stat(name)
|
||||
}
|
|
@ -3,56 +3,49 @@ package fun
|
|||
import "time"
|
||||
import "math"
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
import "image/color"
|
||||
import "tomo"
|
||||
import "art"
|
||||
import "art/shapes"
|
||||
|
||||
var clockCase = theme.C("fun", "clock")
|
||||
var clockCase = tomo.C("tomo", "clock")
|
||||
|
||||
// AnalogClock can display the time of day in an analog format.
|
||||
type AnalogClock struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
time time.Time
|
||||
entity tomo.Entity
|
||||
time time.Time
|
||||
}
|
||||
|
||||
// NewAnalogClock creates a new analog clock that displays the specified time.
|
||||
func NewAnalogClock (newTime time.Time) (element *AnalogClock) {
|
||||
element = &AnalogClock { }
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
element.core.SetMinimumSize(64, 64)
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.entity.SetMinimumSize(64, 64)
|
||||
return
|
||||
}
|
||||
|
||||
// SetTime changes the time that the clock displays.
|
||||
func (element *AnalogClock) SetTime (newTime time.Time) {
|
||||
if newTime == element.time { return }
|
||||
element.time = newTime
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
// Entity returns this element's entity.
|
||||
func (element *AnalogClock) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
func (element *AnalogClock) draw () {
|
||||
bounds := element.Bounds()
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *AnalogClock) Draw (destination art.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
|
||||
pattern, inset := theme.SunkenPattern(theme.PatternState {
|
||||
Case: clockCase,
|
||||
})
|
||||
artist.FillRectangle(element, pattern, bounds)
|
||||
state := tomo.State { }
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternSunken, state, clockCase)
|
||||
padding := element.entity.Theme().Padding(tomo.PatternSunken, clockCase)
|
||||
pattern.Draw(destination, bounds)
|
||||
|
||||
bounds = inset.Apply(bounds)
|
||||
bounds = padding.Apply(bounds)
|
||||
|
||||
foreground, _ := theme.ForegroundPattern(theme.PatternState {
|
||||
Case: clockCase,
|
||||
})
|
||||
accent, _ := theme.AccentPattern(theme.PatternState {
|
||||
Case: clockCase,
|
||||
})
|
||||
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, clockCase)
|
||||
accent := element.entity.Theme().Color(tomo.ColorAccent, state, clockCase)
|
||||
|
||||
for hour := 0; hour < 12; hour ++ {
|
||||
element.radialLine (
|
||||
destination,
|
||||
foreground,
|
||||
0.8, 0.9, float64(hour) / 6 * math.Pi)
|
||||
}
|
||||
|
@ -61,35 +54,37 @@ func (element *AnalogClock) draw () {
|
|||
minute := float64(element.time.Minute()) + second / 60
|
||||
hour := float64(element.time.Hour()) + minute / 60
|
||||
|
||||
element.radialLine(foreground, 0, 0.5, (hour - 3) / 6 * math.Pi)
|
||||
element.radialLine(foreground, 0, 0.7, (minute - 15) / 30 * math.Pi)
|
||||
element.radialLine(accent, 0, 0.7, (second - 15) / 30 * math.Pi)
|
||||
element.radialLine(destination, foreground, 0, 0.5, (hour - 3) / 6 * math.Pi)
|
||||
element.radialLine(destination, foreground, 0, 0.7, (minute - 15) / 30 * math.Pi)
|
||||
element.radialLine(destination, accent, 0, 0.7, (second - 15) / 30 * math.Pi)
|
||||
}
|
||||
|
||||
// FlexibleHeightFor constrains the clock's minimum size to a 1:1 aspect ratio.
|
||||
func (element *AnalogClock) FlexibleHeightFor (width int) (height int) {
|
||||
return width
|
||||
// SetTime changes the time that the clock displays.
|
||||
func (element *AnalogClock) SetTime (newTime time.Time) {
|
||||
if newTime == element.time { return }
|
||||
element.time = newTime
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// OnFlexibleHeightChange sets a function to be calle dwhen the parameters
|
||||
// affecting the clock's flexible height change.
|
||||
func (element *AnalogClock) OnFlexibleHeightChange (func ()) { }
|
||||
func (element *AnalogClock) HandleThemeChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *AnalogClock) radialLine (
|
||||
source artist.Pattern,
|
||||
destination art.Canvas,
|
||||
source color.RGBA,
|
||||
inner float64,
|
||||
outer float64,
|
||||
radian float64,
|
||||
) {
|
||||
bounds := element.Bounds()
|
||||
bounds := element.entity.Bounds()
|
||||
width := float64(bounds.Dx()) / 2
|
||||
height := float64(bounds.Dy()) / 2
|
||||
min := element.Bounds().Min.Add(image.Pt (
|
||||
min := bounds.Min.Add(image.Pt (
|
||||
int(math.Cos(radian) * inner * width + width),
|
||||
int(math.Sin(radian) * inner * height + height)))
|
||||
max := element.Bounds().Min.Add(image.Pt (
|
||||
max := bounds.Min.Add(image.Pt (
|
||||
int(math.Cos(radian) * outer * width + width),
|
||||
int(math.Sin(radian) * outer * height + height)))
|
||||
// println(min.String(), max.String())
|
||||
artist.Line(element, source, 1, min, max)
|
||||
shapes.ColorLine(destination, source, 1, min, max)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
// Package fun provides "fun" elements that have few actual use cases, but serve
|
||||
// as good demos of what Tomo is capable of.
|
||||
package fun
|
|
@ -0,0 +1,4 @@
|
|||
// Package music provides types relating to music theory and the math behind it.
|
||||
// It is used in the fun.Piano element, and in the piano example to generate
|
||||
// pitches from notes.
|
||||
package music
|
|
@ -0,0 +1,70 @@
|
|||
package music
|
||||
|
||||
import "math"
|
||||
|
||||
var semitone = math.Pow(2, 1.0 / 12.0)
|
||||
|
||||
// Tuning is an interface representing a tuning.
|
||||
type Tuning interface {
|
||||
// Tune returns the frequency of a given note in Hz.
|
||||
Tune (Note) float64
|
||||
}
|
||||
|
||||
// EqualTemparment implements twelve-tone equal temparment.
|
||||
type EqualTemparment struct { A4 float64 }
|
||||
|
||||
// Tune returns the EqualTemparment frequency of a given note in Hz.
|
||||
func (tuning EqualTemparment) Tune (note Note) float64 {
|
||||
return tuning.A4 * math.Pow(semitone, float64(note - NoteA4))
|
||||
}
|
||||
|
||||
// Octave represents a MIDI octave.
|
||||
type Octave int
|
||||
|
||||
// Note returns the note at the specified scale degree in the chromatic scale.
|
||||
func (octave Octave) Note (degree int) Note {
|
||||
return Note(int(octave + 1) * 12 + degree)
|
||||
}
|
||||
|
||||
// Note represents a MIDI note.
|
||||
type Note int
|
||||
|
||||
const (
|
||||
NoteC0 Note = iota
|
||||
NoteDb0
|
||||
NoteD0
|
||||
NoteEb0
|
||||
NoteE0
|
||||
NoteF0
|
||||
NoteGb0
|
||||
NoteG0
|
||||
NoteAb0
|
||||
NoteA0
|
||||
NoteBb0
|
||||
NoteB0
|
||||
|
||||
// nice
|
||||
NoteA4 Note = 69
|
||||
)
|
||||
|
||||
// Octave returns the octave of the note
|
||||
func (note Note) Octave () Octave {
|
||||
return Octave(note / 12 - 1)
|
||||
}
|
||||
|
||||
// Degree returns the scale degree of the note in the chromatic scale.
|
||||
func (note Note) Degree () int {
|
||||
mod := note % 12
|
||||
if mod < 0 { mod += 12 }
|
||||
return int(mod)
|
||||
}
|
||||
|
||||
// IsSharp returns whether or not the note is a sharp.
|
||||
func (note Note) IsSharp () bool {
|
||||
degree := note.Degree()
|
||||
return degree == 1 ||
|
||||
degree == 3 ||
|
||||
degree == 6 ||
|
||||
degree == 8 ||
|
||||
degree == 10
|
||||
}
|
|
@ -0,0 +1,326 @@
|
|||
package fun
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
import "art/artutil"
|
||||
import "tomo/elements/fun/music"
|
||||
|
||||
var pianoCase = tomo.C("tomo", "piano")
|
||||
var flatCase = tomo.C("tomo", "piano", "flatKey")
|
||||
var sharpCase = tomo.C("tomo", "piano", "sharpKey")
|
||||
|
||||
const pianoKeyWidth = 18
|
||||
|
||||
type pianoKey struct {
|
||||
image.Rectangle
|
||||
music.Note
|
||||
}
|
||||
|
||||
// Piano is an element that can be used to input midi notes.
|
||||
type Piano struct {
|
||||
entity tomo.Entity
|
||||
|
||||
low, high music.Octave
|
||||
flatKeys []pianoKey
|
||||
sharpKeys []pianoKey
|
||||
contentBounds image.Rectangle
|
||||
|
||||
enabled bool
|
||||
pressed *pianoKey
|
||||
keynavPressed map[music.Note] bool
|
||||
|
||||
onPress func (music.Note)
|
||||
onRelease func (music.Note)
|
||||
}
|
||||
|
||||
// NewPiano returns a new piano element with a lowest and highest octave,
|
||||
// inclusive. If low is greater than high, they will be swapped.
|
||||
func NewPiano (low, high music.Octave) (element *Piano) {
|
||||
if low > high { low, high = high, low }
|
||||
|
||||
element = &Piano {
|
||||
low: low,
|
||||
high: high,
|
||||
keynavPressed: make(map[music.Note] bool),
|
||||
}
|
||||
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Piano) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Piano) Draw (destination art.Canvas) {
|
||||
element.recalculate()
|
||||
|
||||
state := tomo.State {
|
||||
Focused: element.entity.Focused(),
|
||||
Disabled: !element.Enabled(),
|
||||
}
|
||||
|
||||
for _, key := range element.flatKeys {
|
||||
_, keynavPressed := element.keynavPressed[key.Note]
|
||||
element.drawFlat (
|
||||
destination,
|
||||
key.Rectangle,
|
||||
element.pressed != nil &&
|
||||
(*element.pressed).Note == key.Note || keynavPressed,
|
||||
state)
|
||||
}
|
||||
for _, key := range element.sharpKeys {
|
||||
_, keynavPressed := element.keynavPressed[key.Note]
|
||||
element.drawSharp (
|
||||
destination,
|
||||
key.Rectangle,
|
||||
element.pressed != nil &&
|
||||
(*element.pressed).Note == key.Note || keynavPressed,
|
||||
state)
|
||||
}
|
||||
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternPinboard, state, pianoCase)
|
||||
artutil.DrawShatter (
|
||||
destination, pattern, element.entity.Bounds(),
|
||||
element.contentBounds)
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *Piano) Focus () {
|
||||
element.entity.Focus()
|
||||
}
|
||||
|
||||
// Enabled returns whether this piano can be played or not.
|
||||
func (element *Piano) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this piano can be played or not.
|
||||
func (element *Piano) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
|
||||
// OnPress sets a function to be called when a key is pressed.
|
||||
func (element *Piano) OnPress (callback func (note music.Note)) {
|
||||
element.onPress = callback
|
||||
}
|
||||
|
||||
// OnRelease sets a function to be called when a key is released.
|
||||
func (element *Piano) OnRelease (callback func (note music.Note)) {
|
||||
element.onRelease = callback
|
||||
}
|
||||
|
||||
func (element *Piano) HandleMouseDown (x, y int, button input.Button) {
|
||||
element.Focus()
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressUnderMouseCursor(image.Pt(x, y))
|
||||
}
|
||||
|
||||
func (element *Piano) HandleMouseUp (x, y int, button input.Button) {
|
||||
if button != input.ButtonLeft { return }
|
||||
if element.onRelease != nil && element.pressed != nil {
|
||||
element.onRelease((*element.pressed).Note)
|
||||
}
|
||||
element.pressed = nil
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Piano) HandleMotion (x, y int) {
|
||||
if element.pressed == nil { return }
|
||||
element.pressUnderMouseCursor(image.Pt(x, y))
|
||||
}
|
||||
|
||||
func (element *Piano) pressUnderMouseCursor (point image.Point) {
|
||||
// find out which note is being pressed
|
||||
newKey := (*pianoKey)(nil)
|
||||
for index, key := range element.flatKeys {
|
||||
if point.In(key.Rectangle) {
|
||||
newKey = &element.flatKeys[index]
|
||||
break
|
||||
}
|
||||
}
|
||||
for index, key := range element.sharpKeys {
|
||||
if point.In(key.Rectangle) {
|
||||
newKey = &element.sharpKeys[index]
|
||||
break
|
||||
}
|
||||
}
|
||||
if newKey == nil { return }
|
||||
|
||||
if newKey != element.pressed {
|
||||
// release previous note
|
||||
if element.pressed != nil && element.onRelease != nil {
|
||||
element.onRelease((*element.pressed).Note)
|
||||
}
|
||||
|
||||
// press new note
|
||||
element.pressed = newKey
|
||||
if element.onPress != nil {
|
||||
element.onPress((*element.pressed).Note)
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
var noteForKey = map[input.Key] music.Note {
|
||||
'a': 46,
|
||||
'z': 47,
|
||||
|
||||
'x': 48,
|
||||
'd': 49,
|
||||
'c': 50,
|
||||
'f': 51,
|
||||
'v': 52,
|
||||
'b': 53,
|
||||
'h': 54,
|
||||
'n': 55,
|
||||
'j': 56,
|
||||
'm': 57,
|
||||
'k': 58,
|
||||
',': 59,
|
||||
'.': 60,
|
||||
';': 61,
|
||||
'/': 62,
|
||||
'\'': 63,
|
||||
|
||||
'1': 56,
|
||||
'q': 57,
|
||||
'2': 58,
|
||||
'w': 59,
|
||||
|
||||
'e': 60,
|
||||
'4': 61,
|
||||
'r': 62,
|
||||
'5': 63,
|
||||
't': 64,
|
||||
'y': 65,
|
||||
'7': 66,
|
||||
'u': 67,
|
||||
'8': 68,
|
||||
'i': 69,
|
||||
'9': 70,
|
||||
'o': 71,
|
||||
|
||||
'p': 72,
|
||||
'-': 73,
|
||||
'[': 74,
|
||||
'=': 75,
|
||||
']': 76,
|
||||
'\\': 77,
|
||||
}
|
||||
|
||||
func (element *Piano) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
note, exists := noteForKey[key]
|
||||
if !exists { return }
|
||||
if !element.keynavPressed[note] {
|
||||
element.keynavPressed[note] = true
|
||||
if element.onPress != nil {
|
||||
element.onPress(note)
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Piano) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
|
||||
note, exists := noteForKey[key]
|
||||
if !exists { return }
|
||||
_, pressed := element.keynavPressed[note]
|
||||
if !pressed { return }
|
||||
delete(element.keynavPressed, note)
|
||||
if element.onRelease != nil {
|
||||
element.onRelease(note)
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Piano) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Piano) updateMinimumSize () {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternPinboard, pianoCase)
|
||||
element.entity.SetMinimumSize (
|
||||
pianoKeyWidth * 7 * element.countOctaves() +
|
||||
padding.Horizontal(),
|
||||
64 + padding.Vertical())
|
||||
}
|
||||
|
||||
func (element *Piano) countOctaves () int {
|
||||
return int(element.high - element.low + 1)
|
||||
}
|
||||
|
||||
func (element *Piano) countFlats () int {
|
||||
return element.countOctaves() * 8
|
||||
}
|
||||
|
||||
func (element *Piano) countSharps () int {
|
||||
return element.countOctaves() * 5
|
||||
}
|
||||
|
||||
func (element *Piano) recalculate () {
|
||||
element.flatKeys = make([]pianoKey, element.countFlats())
|
||||
element.sharpKeys = make([]pianoKey, element.countSharps())
|
||||
|
||||
padding := element.entity.Theme().Padding(tomo.PatternPinboard, pianoCase)
|
||||
bounds := padding.Apply(element.entity.Bounds())
|
||||
|
||||
dot := bounds.Min
|
||||
note := element.low.Note(0)
|
||||
limit := element.high.Note(12)
|
||||
flatIndex := 0
|
||||
sharpIndex := 0
|
||||
for note < limit {
|
||||
if note.IsSharp() {
|
||||
element.sharpKeys[sharpIndex].Rectangle = image.Rect (
|
||||
-(pianoKeyWidth * 3) / 7, 0,
|
||||
(pianoKeyWidth * 3) / 7,
|
||||
(bounds.Dy() * 5) / 8).Add(dot)
|
||||
element.sharpKeys[sharpIndex].Note = note
|
||||
sharpIndex ++
|
||||
} else {
|
||||
element.flatKeys[flatIndex].Rectangle = image.Rect (
|
||||
0, 0, pianoKeyWidth, bounds.Dy()).Add(dot)
|
||||
dot.X += pianoKeyWidth
|
||||
element.flatKeys[flatIndex].Note = note
|
||||
flatIndex ++
|
||||
}
|
||||
note ++
|
||||
}
|
||||
|
||||
element.contentBounds = image.Rectangle {
|
||||
bounds.Min,
|
||||
image.Pt(dot.X, bounds.Max.Y),
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Piano) drawFlat (
|
||||
destination art.Canvas,
|
||||
bounds image.Rectangle,
|
||||
pressed bool,
|
||||
state tomo.State,
|
||||
) {
|
||||
state.Pressed = pressed
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, flatCase)
|
||||
pattern.Draw(destination, bounds)
|
||||
}
|
||||
|
||||
func (element *Piano) drawSharp (
|
||||
destination art.Canvas,
|
||||
bounds image.Rectangle,
|
||||
pressed bool,
|
||||
state tomo.State,
|
||||
) {
|
||||
state.Pressed = pressed
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, sharpCase)
|
||||
pattern.Draw(destination, bounds)
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "art"
|
||||
|
||||
var iconCase = tomo.C("tomo", "icon")
|
||||
|
||||
// Icon is an element capable of displaying a singular icon.
|
||||
type Icon struct {
|
||||
entity tomo.Entity
|
||||
id tomo.Icon
|
||||
size tomo.IconSize
|
||||
}
|
||||
|
||||
// Icon creates a new icon element.
|
||||
func NewIcon (id tomo.Icon, size tomo.IconSize) (element *Icon) {
|
||||
element = &Icon {
|
||||
id: id,
|
||||
size: size,
|
||||
}
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Icon) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// SetIcon sets the element's icon.
|
||||
func (element *Icon) SetIcon (id tomo.Icon, size tomo.IconSize) {
|
||||
element.id = id
|
||||
element.size = size
|
||||
if element.entity == nil { return }
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Icon) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Icon) Draw (destination art.Canvas) {
|
||||
if element.entity == nil { return }
|
||||
|
||||
bounds := element.entity.Bounds()
|
||||
state := tomo.State { }
|
||||
element.entity.Theme().
|
||||
Pattern(tomo.PatternBackground, state, iconCase).
|
||||
Draw(destination, bounds)
|
||||
icon := element.icon()
|
||||
if icon != nil {
|
||||
iconBounds := icon.Bounds()
|
||||
offset := image.Pt (
|
||||
(bounds.Dx() - iconBounds.Dx()) / 2,
|
||||
(bounds.Dy() - iconBounds.Dy()) / 2)
|
||||
icon.Draw (
|
||||
destination,
|
||||
element.entity.Theme().Color(tomo.ColorForeground, state, iconCase),
|
||||
bounds.Min.Add(offset))
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Icon) icon () art.Icon {
|
||||
return element.entity.Theme().Icon(element.id, element.size, iconCase)
|
||||
}
|
||||
|
||||
func (element *Icon) updateMinimumSize () {
|
||||
icon := element.icon()
|
||||
if icon == nil {
|
||||
element.entity.SetMinimumSize(0, 0)
|
||||
} else {
|
||||
bounds := icon.Bounds()
|
||||
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "art"
|
||||
import "art/patterns"
|
||||
|
||||
// TODO: this element is lame need to make it better
|
||||
|
||||
// Image is an element capable of displaying an image.
|
||||
type Image struct {
|
||||
entity tomo.Entity
|
||||
buffer art.Canvas
|
||||
}
|
||||
|
||||
// NewImage creates a new image element.
|
||||
func NewImage (image image.Image) (element *Image) {
|
||||
element = &Image { buffer: art.FromImage(image) }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
bounds := element.buffer.Bounds()
|
||||
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Image) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Image) Draw (destination art.Canvas) {
|
||||
if element.entity == nil { return }
|
||||
(patterns.Texture { Canvas: element.buffer }).
|
||||
Draw(destination, element.entity.Bounds())
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "golang.org/x/image/math/fixed"
|
||||
import "tomo"
|
||||
import "tomo/data"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
import "tomo/textdraw"
|
||||
|
||||
var labelCase = tomo.C("tomo", "label")
|
||||
|
||||
// Label is a simple text box.
|
||||
type Label struct {
|
||||
entity tomo.Entity
|
||||
|
||||
align textdraw.Align
|
||||
wrap bool
|
||||
text string
|
||||
drawer textdraw.Drawer
|
||||
|
||||
forcedColumns int
|
||||
forcedRows int
|
||||
minHeight int
|
||||
}
|
||||
|
||||
// NewLabel creates a new label.
|
||||
func NewLabel (text string) (element *Label) {
|
||||
element = &Label { }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal, labelCase))
|
||||
element.SetText(text)
|
||||
return
|
||||
}
|
||||
|
||||
// NewLabelWrapped creates a new label with text wrapping on.
|
||||
func NewLabelWrapped (text string) (element *Label) {
|
||||
element = NewLabel(text)
|
||||
element.SetWrap(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Label) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Label) Draw (destination art.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
|
||||
if element.wrap {
|
||||
element.drawer.SetMaxWidth(bounds.Dx())
|
||||
element.drawer.SetMaxHeight(bounds.Dy())
|
||||
}
|
||||
|
||||
element.entity.DrawBackground(destination)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
foreground := element.entity.Theme().Color (
|
||||
tomo.ColorForeground,
|
||||
tomo.State { }, labelCase)
|
||||
element.drawer.Draw(destination, foreground, bounds.Min.Sub(textBounds.Min))
|
||||
}
|
||||
|
||||
// Copy copies the label's textto the clipboard.
|
||||
func (element *Label) Copy () {
|
||||
window := element.entity.Window()
|
||||
if window != nil {
|
||||
window.Copy(data.Bytes(data.MimePlain, []byte(element.text)))
|
||||
}
|
||||
}
|
||||
|
||||
// EmCollapse forces a minimum width and height upon the label. The width is
|
||||
// measured in emspaces, and the height is measured in lines. If a zero value is
|
||||
// given for a dimension, its minimum will be determined by the label's content.
|
||||
// If the label's content is greater than these dimensions, it will be truncated
|
||||
// to fit.
|
||||
func (element *Label) EmCollapse (columns int, rows int) {
|
||||
element.forcedColumns = columns
|
||||
element.forcedRows = rows
|
||||
element.updateMinimumSize()
|
||||
}
|
||||
|
||||
// FlexibleHeightFor returns the reccomended height for this element based on
|
||||
// the given width in order to allow the text to wrap properly.
|
||||
func (element *Label) FlexibleHeightFor (width int) (height int) {
|
||||
if element.wrap {
|
||||
return element.drawer.ReccomendedHeightFor(width)
|
||||
} else {
|
||||
return element.minHeight
|
||||
}
|
||||
}
|
||||
|
||||
// SetText sets the label's text.
|
||||
func (element *Label) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetWrap sets wether or not the label's text wraps. If the text is set to
|
||||
// wrap, the element will have a minimum size of a single character and
|
||||
// automatically wrap its text. If the text is set to not wrap, the element will
|
||||
// have a minimum size that fits its text.
|
||||
func (element *Label) SetWrap (wrap bool) {
|
||||
if wrap == element.wrap { return }
|
||||
if !wrap {
|
||||
element.drawer.SetMaxWidth(0)
|
||||
element.drawer.SetMaxHeight(0)
|
||||
}
|
||||
element.wrap = wrap
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetAlign sets the alignment method of the label.
|
||||
func (element *Label) SetAlign (align textdraw.Align) {
|
||||
if align == element.align { return }
|
||||
element.align = align
|
||||
element.drawer.SetAlign(align)
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Label) HandleThemeChange () {
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal, labelCase))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Label) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button == input.ButtonRight {
|
||||
element.contextMenu(position)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Label) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) { }
|
||||
|
||||
func (element *Label) contextMenu (position image.Point) {
|
||||
window := element.entity.Window()
|
||||
menu, err := window.NewMenu(image.Rectangle { position, position })
|
||||
if err != nil { return }
|
||||
|
||||
closeAnd := func (callback func ()) func () {
|
||||
return func () { callback(); menu.Close() }
|
||||
}
|
||||
|
||||
copyButton := NewButton("Copy")
|
||||
copyButton.ShowText(false)
|
||||
copyButton.SetIcon(tomo.IconCopy)
|
||||
copyButton.OnClick(closeAnd(element.Copy))
|
||||
|
||||
menu.Adopt (NewHBox (
|
||||
SpaceNone,
|
||||
copyButton,
|
||||
))
|
||||
copyButton.Focus()
|
||||
menu.Show()
|
||||
}
|
||||
|
||||
func (element *Label) updateMinimumSize () {
|
||||
var width, height int
|
||||
|
||||
if element.wrap {
|
||||
em := element.drawer.Em().Round()
|
||||
if em < 1 {
|
||||
em = element.entity.Theme().Padding(tomo.PatternBackground, labelCase)[0]
|
||||
}
|
||||
width, height = em, element.drawer.LineHeight().Round()
|
||||
element.entity.NotifyFlexibleHeightChange()
|
||||
} else {
|
||||
bounds := element.drawer.LayoutBounds()
|
||||
width, height = bounds.Dx(), bounds.Dy()
|
||||
}
|
||||
|
||||
if element.forcedColumns > 0 {
|
||||
width =
|
||||
element.drawer.Em().
|
||||
Mul(fixed.I(element.forcedColumns)).Floor()
|
||||
}
|
||||
|
||||
if element.forcedRows > 0 {
|
||||
height =
|
||||
element.drawer.LineHeight().
|
||||
Mul(fixed.I(element.forcedRows)).Floor()
|
||||
}
|
||||
|
||||
element.minHeight = height
|
||||
element.entity.SetMinimumSize(width, height)
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package elements
|
||||
|
||||
import "tomo"
|
||||
|
||||
// Numeric is a type constraint representing a number.
|
||||
type Numeric interface {
|
||||
~float32 | ~float64 |
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64 |
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
|
||||
}
|
||||
|
||||
// LerpSlider is a slider that has a minimum and maximum value, and who's value
|
||||
// can be any numeric type.
|
||||
type LerpSlider[T Numeric] struct {
|
||||
slider
|
||||
min T
|
||||
max T
|
||||
}
|
||||
|
||||
// NewVLerpSlider creates a new horizontal LerpSlider with a minimum and maximum
|
||||
// value.
|
||||
func NewVLerpSlider[T Numeric] (min, max T, value T) (element *LerpSlider[T]) {
|
||||
element = NewHLerpSlider(min, max, value)
|
||||
element.vertical = true
|
||||
return
|
||||
}
|
||||
|
||||
// NewHLerpSlider creates a new horizontal LerpSlider with a minimum and maximum
|
||||
// value.
|
||||
func NewHLerpSlider[T Numeric] (min, max T, value T) (element *LerpSlider[T]) {
|
||||
if min > max { min, max = max, min }
|
||||
element = &LerpSlider[T] {
|
||||
min: min,
|
||||
max: max,
|
||||
}
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.construct()
|
||||
element.SetValue(value)
|
||||
return
|
||||
}
|
||||
|
||||
// SetValue sets the slider's value.
|
||||
func (element *LerpSlider[T]) SetValue (value T) {
|
||||
value -= element.min
|
||||
element.slider.SetValue(float64(value) / float64(element.Range()))
|
||||
}
|
||||
|
||||
// Value returns the slider's value.
|
||||
func (element *LerpSlider[T]) Value () (value T) {
|
||||
return T (
|
||||
float64(element.slider.Value()) * float64(element.Range())) +
|
||||
element.min
|
||||
}
|
||||
|
||||
// Range returns the difference between the slider's maximum and minimum values.
|
||||
func (element *LerpSlider[T]) Range () T {
|
||||
return element.max - element.min
|
||||
}
|
|
@ -0,0 +1,448 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
import "tomo/ability"
|
||||
import "art/artutil"
|
||||
|
||||
type list struct {
|
||||
container
|
||||
entity tomo.Entity
|
||||
|
||||
c tomo.Case
|
||||
|
||||
enabled bool
|
||||
scroll image.Point
|
||||
contentBounds image.Rectangle
|
||||
selected int
|
||||
|
||||
forcedMinimumWidth int
|
||||
forcedMinimumHeight int
|
||||
|
||||
onClick func ()
|
||||
onSelectionChange func ()
|
||||
onScrollBoundsChange func ()
|
||||
}
|
||||
|
||||
type List struct {
|
||||
list
|
||||
}
|
||||
|
||||
type FlowList struct {
|
||||
list
|
||||
}
|
||||
|
||||
func NewList (children ...tomo.Element) (element *List) {
|
||||
element = &List { }
|
||||
element.c = tomo.C("tomo", "list")
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.container.entity = element.entity
|
||||
element.minimumSize = element.updateMinimumSize
|
||||
element.init(children...)
|
||||
return
|
||||
}
|
||||
|
||||
func NewFlowList (children ...tomo.Element) (element *FlowList) {
|
||||
element = &FlowList { }
|
||||
element.c = tomo.C("tomo", "flowList")
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.container.entity = element.entity
|
||||
element.minimumSize = element.updateMinimumSize
|
||||
element.init(children...)
|
||||
return
|
||||
}
|
||||
|
||||
func (element *list) init (children ...tomo.Element) {
|
||||
element.selected = -1
|
||||
element.enabled = true
|
||||
element.container.init()
|
||||
element.Adopt(children...)
|
||||
}
|
||||
|
||||
func (element *list) Draw (destination art.Canvas) {
|
||||
rocks := make([]image.Rectangle, element.entity.CountChildren())
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
rocks[index] = element.entity.Child(index).Entity().Bounds()
|
||||
}
|
||||
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternSunken, element.state(), element.c)
|
||||
artutil.DrawShatter(destination, pattern, element.entity.Bounds(), rocks...)
|
||||
}
|
||||
|
||||
func (element *List) Layout () {
|
||||
if element.scroll.Y > element.maxScrollHeight() {
|
||||
element.scroll.Y = element.maxScrollHeight()
|
||||
}
|
||||
|
||||
margin := element.entity.Theme().Margin(tomo.PatternSunken, element.c)
|
||||
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
|
||||
bounds := padding.Apply(element.entity.Bounds())
|
||||
element.contentBounds = image.Rectangle { }
|
||||
|
||||
dot := bounds.Min.Sub(element.scroll)
|
||||
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
child := element.entity.Child(index)
|
||||
entry := element.scratch[child]
|
||||
|
||||
width := bounds.Dx()
|
||||
height := int(entry.minSize)
|
||||
|
||||
childBounds := tomo.Bounds (
|
||||
dot.X, dot.Y,
|
||||
width, height)
|
||||
element.entity.PlaceChild(index, childBounds)
|
||||
element.contentBounds = element.contentBounds.Union(childBounds)
|
||||
|
||||
dot.Y += height
|
||||
dot.Y += margin.Y
|
||||
}
|
||||
|
||||
element.contentBounds =
|
||||
element.contentBounds.Sub(element.contentBounds.Min)
|
||||
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *FlowList) Layout () {
|
||||
if element.scroll.Y > element.maxScrollHeight() {
|
||||
element.scroll.Y = element.maxScrollHeight()
|
||||
}
|
||||
|
||||
margin := element.entity.Theme().Margin(tomo.PatternSunken, element.c)
|
||||
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
|
||||
bounds := padding.Apply(element.entity.Bounds())
|
||||
element.contentBounds = image.Rectangle { }
|
||||
|
||||
dot := bounds.Min.Sub(element.scroll)
|
||||
xStart := dot.X
|
||||
rowHeight := 0
|
||||
|
||||
nextLine := func () {
|
||||
dot.X = xStart
|
||||
dot.Y += margin.Y
|
||||
dot.Y += rowHeight
|
||||
rowHeight = 0
|
||||
}
|
||||
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
child := element.entity.Child(index)
|
||||
entry := element.scratch[child]
|
||||
|
||||
width := int(entry.minBreadth)
|
||||
height := int(entry.minSize)
|
||||
if width + dot.X > bounds.Max.X {
|
||||
nextLine()
|
||||
}
|
||||
if typedChild, ok := child.(ability.Flexible); ok {
|
||||
height = typedChild.FlexibleHeightFor(width)
|
||||
}
|
||||
if rowHeight < height {
|
||||
rowHeight = height
|
||||
}
|
||||
|
||||
childBounds := tomo.Bounds (
|
||||
dot.X, dot.Y,
|
||||
width, height)
|
||||
element.entity.PlaceChild(index, childBounds)
|
||||
element.contentBounds = element.contentBounds.Union(childBounds)
|
||||
|
||||
dot.X += width + margin.X
|
||||
}
|
||||
|
||||
element.contentBounds =
|
||||
element.contentBounds.Sub(element.contentBounds.Min)
|
||||
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *list) Selected () ability.Selectable {
|
||||
if element.selected == -1 { return nil }
|
||||
child, ok := element.entity.Child(element.selected).(ability.Selectable)
|
||||
if !ok { return nil }
|
||||
return child
|
||||
}
|
||||
|
||||
func (element *list) Select (child ability.Selectable) {
|
||||
index := element.entity.IndexOf(child)
|
||||
if element.selected == index { return }
|
||||
element.selectNone()
|
||||
element.selected = index
|
||||
element.entity.SelectChild(index, true)
|
||||
if element.onSelectionChange != nil {
|
||||
element.onSelectionChange()
|
||||
}
|
||||
element.scrollToSelected()
|
||||
}
|
||||
|
||||
func (element *list) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
func (element *list) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *list) Focus () {
|
||||
element.entity.Focus()
|
||||
}
|
||||
|
||||
func (element *list) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *list) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.enabled { return }
|
||||
element.Focus()
|
||||
element.selectNone()
|
||||
}
|
||||
|
||||
func (element *list) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) { }
|
||||
|
||||
func (element *list) HandleChildMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
child tomo.Element,
|
||||
) {
|
||||
if !element.enabled { return }
|
||||
element.Focus()
|
||||
if child, ok := child.(ability.Selectable); ok {
|
||||
element.Select(child)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *list) HandleChildMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
child tomo.Element,
|
||||
) {
|
||||
if !position.In(child.Entity().Bounds()) { return }
|
||||
if element.onClick != nil {
|
||||
element.onClick()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *list) HandleChildFlexibleHeightChange (child ability.Flexible) {
|
||||
element.minimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *list) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
index := -1
|
||||
switch key {
|
||||
case input.KeyUp, input.KeyLeft:
|
||||
index = element.selected - 1
|
||||
case input.KeyDown, input.KeyRight:
|
||||
index = element.selected + 1
|
||||
case input.KeyEnter:
|
||||
if element.onClick != nil {
|
||||
element.onClick()
|
||||
}
|
||||
}
|
||||
if index >= 0 && index < element.entity.CountChildren() {
|
||||
element.selectNone()
|
||||
element.selected = index
|
||||
element.entity.SelectChild(index, true)
|
||||
if element.onSelectionChange != nil {
|
||||
element.onSelectionChange()
|
||||
}
|
||||
element.scrollToSelected()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *list) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
|
||||
|
||||
func (element *list) DrawBackground (destination art.Canvas) {
|
||||
element.entity.DrawBackground(destination)
|
||||
}
|
||||
|
||||
func (element *list) HandleThemeChange () {
|
||||
element.minimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// Collapse forces a minimum width and height upon the list. If a zero value is
|
||||
// given for a dimension, its minimum will be determined by the list's content.
|
||||
// If the list's height goes beyond the forced size, it will need to be accessed
|
||||
// via scrolling. If an entry's width goes beyond the forced size, its text will
|
||||
// be truncated so that it fits.
|
||||
func (element *list) Collapse (width, height int) {
|
||||
if
|
||||
element.forcedMinimumWidth == width &&
|
||||
element.forcedMinimumHeight == height {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
element.forcedMinimumWidth = width
|
||||
element.forcedMinimumHeight = height
|
||||
|
||||
element.minimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// ScrollContentBounds returns the full content size of the element.
|
||||
func (element *list) ScrollContentBounds () image.Rectangle {
|
||||
return element.contentBounds
|
||||
}
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's
|
||||
// viewport relative to ScrollBounds.
|
||||
func (element *list) ScrollViewportBounds () image.Rectangle {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
|
||||
bounds := padding.Apply(element.entity.Bounds())
|
||||
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
|
||||
return bounds
|
||||
}
|
||||
|
||||
// ScrollTo scrolls the viewport to the specified point relative to
|
||||
// ScrollBounds.
|
||||
func (element *list) ScrollTo (position image.Point) {
|
||||
if position.Y < 0 {
|
||||
position.Y = 0
|
||||
}
|
||||
maxScrollHeight := element.maxScrollHeight()
|
||||
if position.Y > maxScrollHeight {
|
||||
position.Y = maxScrollHeight
|
||||
}
|
||||
element.scroll = position
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// OnScrollBoundsChange sets a function to be called when the element's viewport
|
||||
// bounds, content bounds, or scroll axes change.
|
||||
func (element *list) OnScrollBoundsChange (callback func ()) {
|
||||
element.onScrollBoundsChange = callback
|
||||
}
|
||||
|
||||
func (element *list) OnClick (callback func ()) {
|
||||
element.onClick = callback
|
||||
}
|
||||
|
||||
func (element *list) OnSelectionChange (callback func ()) {
|
||||
element.onSelectionChange = callback
|
||||
}
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
func (element *list) ScrollAxes () (horizontal, vertical bool) {
|
||||
return false, true
|
||||
}
|
||||
|
||||
func (element *list) selectNone () {
|
||||
if element.selected >= 0 {
|
||||
element.entity.SelectChild(element.selected, false)
|
||||
if element.onSelectionChange != nil {
|
||||
element.onSelectionChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *list) scrollToSelected () {
|
||||
if element.selected < 0 { return }
|
||||
target := element.entity.Child(element.selected).Entity().Bounds()
|
||||
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
|
||||
bounds := padding.Apply(element.entity.Bounds())
|
||||
if target.Min.Y < bounds.Min.Y {
|
||||
element.scroll.Y -= bounds.Min.Y - target.Min.Y
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
} else if target.Max.Y > bounds.Max.Y {
|
||||
element.scroll.Y += target.Max.Y - bounds.Max.Y
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *list) state () tomo.State {
|
||||
return tomo.State {
|
||||
Focused: element.entity.Focused(),
|
||||
Disabled: !element.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func (element *list) maxScrollHeight () (height int) {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
|
||||
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
|
||||
height = element.contentBounds.Dy() - viewportHeight
|
||||
if height < 0 { height = 0 }
|
||||
return
|
||||
}
|
||||
|
||||
func (element *List) updateMinimumSize () {
|
||||
margin := element.entity.Theme().Margin(tomo.PatternSunken, element.c)
|
||||
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
|
||||
|
||||
width := 0
|
||||
height := 0
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
if index > 0 { height += margin.Y }
|
||||
|
||||
child := element.entity.Child(index)
|
||||
entry := element.scratch[child]
|
||||
|
||||
entryWidth, entryHeight := element.entity.ChildMinimumSize(index)
|
||||
entry.minBreadth = float64(entryWidth)
|
||||
entry.minSize = float64(entryHeight)
|
||||
element.scratch[child] = entry
|
||||
|
||||
height += entryHeight
|
||||
if width < entryWidth { width = entryWidth }
|
||||
}
|
||||
|
||||
width += padding.Horizontal()
|
||||
height += padding.Vertical()
|
||||
|
||||
if element.forcedMinimumWidth > 0 {
|
||||
width = element.forcedMinimumWidth
|
||||
}
|
||||
if element.forcedMinimumHeight > 0 {
|
||||
height = element.forcedMinimumHeight
|
||||
}
|
||||
|
||||
element.entity.SetMinimumSize(width, height)
|
||||
}
|
||||
|
||||
func (element *FlowList) updateMinimumSize () {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
|
||||
minimumWidth := 0
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
width, height := element.entity.ChildMinimumSize(index)
|
||||
if width > minimumWidth {
|
||||
minimumWidth = width
|
||||
}
|
||||
|
||||
key := element.entity.Child(index)
|
||||
entry := element.scratch[key]
|
||||
entry.minSize = float64(height)
|
||||
entry.minBreadth = float64(width)
|
||||
element.scratch[key] = entry
|
||||
}
|
||||
element.entity.SetMinimumSize (
|
||||
minimumWidth + padding.Horizontal(),
|
||||
padding.Vertical())
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "art"
|
||||
|
||||
var progressBarCase = tomo.C("tomo", "progressBar")
|
||||
|
||||
// ProgressBar displays a visual indication of how far along a task is.
|
||||
type ProgressBar struct {
|
||||
entity tomo.Entity
|
||||
progress float64
|
||||
}
|
||||
|
||||
// NewProgressBar creates a new progress bar displaying the given progress
|
||||
// level.
|
||||
func NewProgressBar (progress float64) (element *ProgressBar) {
|
||||
if progress < 0 { progress = 0 }
|
||||
if progress > 1 { progress = 1 }
|
||||
element = &ProgressBar { progress: progress }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *ProgressBar) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *ProgressBar) Draw (destination art.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternSunken, tomo.State { }, progressBarCase)
|
||||
padding := element.entity.Theme().Padding(tomo.PatternSunken, progressBarCase)
|
||||
pattern.Draw(destination, bounds)
|
||||
bounds = padding.Apply(bounds)
|
||||
meterBounds := image.Rect (
|
||||
bounds.Min.X, bounds.Min.Y,
|
||||
bounds.Min.X + int(float64(bounds.Dx()) * element.progress),
|
||||
bounds.Max.Y)
|
||||
mercury := element.entity.Theme().Pattern(tomo.PatternMercury, tomo.State { }, progressBarCase)
|
||||
mercury.Draw(destination, meterBounds)
|
||||
}
|
||||
|
||||
// SetProgress sets the progress level of the bar.
|
||||
func (element *ProgressBar) SetProgress (progress float64) {
|
||||
if progress < 0 { progress = 0 }
|
||||
if progress > 1 { progress = 1 }
|
||||
if progress == element.progress { return }
|
||||
element.progress = progress
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ProgressBar) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ProgressBar) updateMinimumSize() {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternSunken, progressBarCase)
|
||||
innerPadding := element.entity.Theme().Padding(tomo.PatternMercury, progressBarCase)
|
||||
element.entity.SetMinimumSize (
|
||||
padding.Horizontal() + innerPadding.Horizontal(),
|
||||
padding.Vertical() + innerPadding.Vertical())
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
import "tomo/ability"
|
||||
|
||||
var scrollCase = tomo.C("tomo", "scroll")
|
||||
|
||||
// ScrollMode specifies which sides of a Scroll have scroll bars.
|
||||
type ScrollMode int; const (
|
||||
ScrollNeither ScrollMode = 0
|
||||
ScrollVertical ScrollMode = 1
|
||||
ScrollHorizontal ScrollMode = 2
|
||||
ScrollBoth ScrollMode = ScrollVertical | ScrollHorizontal
|
||||
)
|
||||
|
||||
// Includes returns whether a scroll mode has been or'd with another scroll
|
||||
// mode.
|
||||
func (mode ScrollMode) Includes (sub ScrollMode) bool {
|
||||
return (mode & sub) > 0
|
||||
}
|
||||
|
||||
// Scroll adds scroll bars to any scrollable element. It also captures scroll
|
||||
// wheel input.
|
||||
type Scroll struct {
|
||||
entity tomo.Entity
|
||||
|
||||
child ability.Scrollable
|
||||
horizontal *ScrollBar
|
||||
vertical *ScrollBar
|
||||
}
|
||||
|
||||
// NewScroll creates a new scroll element.
|
||||
func NewScroll (mode ScrollMode, child ability.Scrollable) (element *Scroll) {
|
||||
element = &Scroll { }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
|
||||
if mode.Includes(ScrollHorizontal) {
|
||||
element.horizontal = NewHScrollBar()
|
||||
element.horizontal.OnScroll (func (viewport image.Point) {
|
||||
if element.child != nil {
|
||||
element.child.ScrollTo(viewport)
|
||||
}
|
||||
if element.vertical != nil {
|
||||
element.vertical.SetBounds (
|
||||
element.child.ScrollContentBounds(),
|
||||
element.child.ScrollViewportBounds())
|
||||
}
|
||||
})
|
||||
element.entity.Adopt(element.horizontal)
|
||||
}
|
||||
if mode.Includes(ScrollVertical) {
|
||||
element.vertical = NewVScrollBar()
|
||||
element.vertical.OnScroll (func (viewport image.Point) {
|
||||
if element.child != nil {
|
||||
element.child.ScrollTo(viewport)
|
||||
}
|
||||
if element.horizontal != nil {
|
||||
element.horizontal.SetBounds (
|
||||
element.child.ScrollContentBounds(),
|
||||
element.child.ScrollViewportBounds())
|
||||
}
|
||||
})
|
||||
element.entity.Adopt(element.vertical)
|
||||
}
|
||||
|
||||
element.Adopt(child)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Scroll) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Scroll) Draw (destination art.Canvas) {
|
||||
if element.horizontal != nil && element.vertical != nil {
|
||||
bounds := element.entity.Bounds()
|
||||
bounds.Min = image.Pt (
|
||||
bounds.Max.X - element.vertical.Entity().Bounds().Dx(),
|
||||
bounds.Max.Y - element.horizontal.Entity().Bounds().Dy())
|
||||
state := tomo.State { }
|
||||
deadArea := element.entity.Theme().Pattern(tomo.PatternDead, state, scrollCase)
|
||||
deadArea.Draw(art.Cut(destination, bounds), bounds)
|
||||
}
|
||||
}
|
||||
|
||||
// Layout causes this element to perform a layout operation.
|
||||
func (element *Scroll) Layout () {
|
||||
bounds := element.entity.Bounds()
|
||||
child := bounds
|
||||
|
||||
iHorizontal := element.entity.IndexOf(element.horizontal)
|
||||
iVertical := element.entity.IndexOf(element.vertical)
|
||||
iChild := element.entity.IndexOf(element.child)
|
||||
|
||||
var horizontal, vertical image.Rectangle
|
||||
|
||||
if element.horizontal != nil {
|
||||
_, hMinHeight := element.entity.ChildMinimumSize(iHorizontal)
|
||||
child.Max.Y -= hMinHeight
|
||||
}
|
||||
if element.vertical != nil {
|
||||
vMinWidth, _ := element.entity.ChildMinimumSize(iVertical)
|
||||
child.Max.X -= vMinWidth
|
||||
}
|
||||
|
||||
horizontal.Min.X = bounds.Min.X
|
||||
horizontal.Max.X = child.Max.X
|
||||
horizontal.Min.Y = child.Max.Y
|
||||
horizontal.Max.Y = bounds.Max.Y
|
||||
|
||||
vertical.Min.X = child.Max.X
|
||||
vertical.Max.X = bounds.Max.X
|
||||
vertical.Min.Y = bounds.Min.Y
|
||||
vertical.Max.Y = child.Max.Y
|
||||
|
||||
if element.horizontal != nil {
|
||||
element.entity.PlaceChild (iHorizontal, horizontal)
|
||||
}
|
||||
if element.vertical != nil {
|
||||
element.entity.PlaceChild(iVertical, vertical)
|
||||
}
|
||||
if element.child != nil {
|
||||
element.entity.PlaceChild(iChild, child)
|
||||
}
|
||||
}
|
||||
|
||||
// DrawBackground draws this element's background pattern to the specified
|
||||
// destination canvas.
|
||||
func (element *Scroll) DrawBackground (destination art.Canvas) {
|
||||
element.entity.DrawBackground(destination)
|
||||
}
|
||||
|
||||
// Adopt sets this element's child. If nil is passed, any child is removed.
|
||||
func (element *Scroll) Adopt (child ability.Scrollable) {
|
||||
if element.child != nil {
|
||||
element.entity.Disown(element.entity.IndexOf(element.child))
|
||||
}
|
||||
if child != nil {
|
||||
element.entity.Adopt(child)
|
||||
}
|
||||
element.child = child
|
||||
|
||||
element.updateEnabled()
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// Child returns this element's child. If there is no child, this method will
|
||||
// return nil.
|
||||
func (element *Scroll) Child () ability.Scrollable {
|
||||
return element.child
|
||||
}
|
||||
|
||||
func (element *Scroll) HandleChildMinimumSizeChange (tomo.Element) {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *Scroll) HandleChildScrollBoundsChange (ability.Scrollable) {
|
||||
element.updateEnabled()
|
||||
viewportBounds := element.child.ScrollViewportBounds()
|
||||
contentBounds := element.child.ScrollContentBounds()
|
||||
if element.horizontal != nil {
|
||||
element.horizontal.SetBounds(contentBounds, viewportBounds)
|
||||
}
|
||||
if element.vertical != nil {
|
||||
element.vertical.SetBounds(contentBounds, viewportBounds)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Scroll) HandleScroll (
|
||||
position image.Point,
|
||||
deltaX, deltaY float64,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
horizontal, vertical := element.child.ScrollAxes()
|
||||
if !horizontal { deltaX = 0 }
|
||||
if !vertical { deltaY = 0 }
|
||||
element.scrollChildBy(int(deltaX), int(deltaY))
|
||||
}
|
||||
|
||||
func (element *Scroll) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *Scroll) updateMinimumSize () {
|
||||
var width, height int
|
||||
|
||||
if element.child != nil {
|
||||
width, height = element.entity.ChildMinimumSize (
|
||||
element.entity.IndexOf(element.child))
|
||||
}
|
||||
if element.horizontal != nil {
|
||||
hMinWidth, hMinHeight := element.entity.ChildMinimumSize (
|
||||
element.entity.IndexOf(element.horizontal))
|
||||
height += hMinHeight
|
||||
if hMinWidth > width {
|
||||
width = hMinWidth
|
||||
}
|
||||
}
|
||||
if element.vertical != nil {
|
||||
vMinWidth, vMinHeight := element.entity.ChildMinimumSize (
|
||||
element.entity.IndexOf(element.vertical))
|
||||
width += vMinWidth
|
||||
if vMinHeight > height {
|
||||
height = vMinHeight
|
||||
}
|
||||
}
|
||||
element.entity.SetMinimumSize(width, height)
|
||||
}
|
||||
|
||||
func (element *Scroll) updateEnabled () {
|
||||
horizontal, vertical := false, false
|
||||
if element.child != nil {
|
||||
horizontal, vertical = element.child.ScrollAxes()
|
||||
}
|
||||
if element.horizontal != nil {
|
||||
element.horizontal.SetEnabled(horizontal)
|
||||
}
|
||||
if element.vertical != nil {
|
||||
element.vertical.SetEnabled(vertical)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Scroll) scrollChildBy (x, y int) {
|
||||
if element.child == nil { return }
|
||||
scrollPoint :=
|
||||
element.child.ScrollViewportBounds().Min.
|
||||
Add(image.Pt(x, y))
|
||||
element.child.ScrollTo(scrollPoint)
|
||||
}
|
|
@ -0,0 +1,321 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
|
||||
// ScrollBar is an element similar to Slider, but it has special behavior that
|
||||
// makes it well suited for controlling the viewport position on one axis of a
|
||||
// scrollable element. Instead of having a value from zero to one, it stores
|
||||
// viewport and content boundaries. When the user drags the scroll bar handle,
|
||||
// the scroll bar calls the OnScroll callback assigned to it with the position
|
||||
// the user is trying to move the handle to. A program can check to see if this
|
||||
// value is valid, move the viewport, and give the scroll bar the new viewport
|
||||
// bounds (which will then cause it to move the handle).
|
||||
//
|
||||
// Typically, you wont't want to use a ScrollBar by itself. A ScrollContainer is
|
||||
// better for most cases.
|
||||
type ScrollBar struct {
|
||||
entity tomo.Entity
|
||||
|
||||
c tomo.Case
|
||||
|
||||
vertical bool
|
||||
enabled bool
|
||||
dragging bool
|
||||
dragOffset image.Point
|
||||
track image.Rectangle
|
||||
bar image.Rectangle
|
||||
|
||||
contentBounds image.Rectangle
|
||||
viewportBounds image.Rectangle
|
||||
|
||||
onScroll func (viewport image.Point)
|
||||
}
|
||||
|
||||
// NewVScrollBar creates a new vertical scroll bar.
|
||||
func NewVScrollBar () (element *ScrollBar) {
|
||||
element = &ScrollBar {
|
||||
vertical: true,
|
||||
enabled: true,
|
||||
}
|
||||
element.c = tomo.C("tomo", "scrollBarVertical")
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
// NewHScrollBar creates a new horizontal scroll bar.
|
||||
func NewHScrollBar () (element *ScrollBar) {
|
||||
element = &ScrollBar {
|
||||
enabled: true,
|
||||
}
|
||||
element.c = tomo.C("tomo", "scrollBarHorizontal")
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *ScrollBar) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *ScrollBar) Draw (destination art.Canvas) {
|
||||
element.recalculate()
|
||||
|
||||
bounds := element.entity.Bounds()
|
||||
state := tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Pressed: element.dragging,
|
||||
}
|
||||
element.entity.Theme().Pattern(tomo.PatternGutter, state, element.c).Draw (
|
||||
destination,
|
||||
bounds)
|
||||
element.entity.Theme().Pattern(tomo.PatternHandle, state, element.c).Draw (
|
||||
destination,
|
||||
element.bar)
|
||||
}
|
||||
|
||||
func (element *ScrollBar) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
velocity := element.entity.Config().ScrollVelocity()
|
||||
|
||||
if position.In(element.bar) {
|
||||
// the mouse is pressed down within the bar's handle
|
||||
element.dragging = true
|
||||
element.entity.Invalidate()
|
||||
element.dragOffset =
|
||||
position.Sub(element.bar.Min).
|
||||
Add(element.entity.Bounds().Min)
|
||||
element.dragTo(position)
|
||||
} else {
|
||||
// the mouse is pressed down within the bar's gutter
|
||||
switch button {
|
||||
case input.ButtonLeft:
|
||||
// start scrolling at this point, but set the offset to
|
||||
// the middle of the handle
|
||||
element.dragging = true
|
||||
element.dragOffset = element.fallbackDragOffset()
|
||||
element.dragTo(position)
|
||||
|
||||
case input.ButtonMiddle:
|
||||
// page up/down on middle click
|
||||
viewport := 0
|
||||
if element.vertical {
|
||||
viewport = element.viewportBounds.Dy()
|
||||
} else {
|
||||
viewport = element.viewportBounds.Dx()
|
||||
}
|
||||
if element.isAfterHandle(position) {
|
||||
element.scrollBy(viewport)
|
||||
} else {
|
||||
element.scrollBy(-viewport)
|
||||
}
|
||||
|
||||
case input.ButtonRight:
|
||||
// inch up/down on right click
|
||||
if element.isAfterHandle(position) {
|
||||
element.scrollBy(velocity)
|
||||
} else {
|
||||
element.scrollBy(-velocity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if element.dragging {
|
||||
element.dragging = false
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) HandleMotion (position image.Point) {
|
||||
if element.dragging {
|
||||
element.dragTo(position)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) HandleScroll (
|
||||
position image.Point,
|
||||
deltaX, deltaY float64,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if element.vertical {
|
||||
element.scrollBy(int(deltaY))
|
||||
} else {
|
||||
element.scrollBy(int(deltaX))
|
||||
}
|
||||
}
|
||||
|
||||
// SetEnabled sets whether or not the scroll bar can be interacted with.
|
||||
func (element *ScrollBar) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// Enabled returns whether or not the element is enabled.
|
||||
func (element *ScrollBar) Enabled () (enabled bool) {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetBounds sets the content and viewport bounds of the scroll bar.
|
||||
func (element *ScrollBar) SetBounds (content, viewport image.Rectangle) {
|
||||
element.contentBounds = content
|
||||
element.viewportBounds = viewport
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// OnScroll sets a function to be called when the user tries to move the scroll
|
||||
// bar's handle. The callback is passed a point representing the new viewport
|
||||
// position. For the scroll bar's position to visually update, the callback must
|
||||
// check if the position is valid and call ScrollBar.SetBounds with the new
|
||||
// viewport bounds.
|
||||
func (element *ScrollBar) OnScroll (callback func (viewport image.Point)) {
|
||||
element.onScroll = callback
|
||||
}
|
||||
|
||||
func (element *ScrollBar) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ScrollBar) isAfterHandle (point image.Point) bool {
|
||||
if element.vertical {
|
||||
return point.Y > element.bar.Min.Y
|
||||
} else {
|
||||
return point.X > element.bar.Min.X
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) fallbackDragOffset () image.Point {
|
||||
if element.vertical {
|
||||
return element.entity.Bounds().Min.
|
||||
Add(image.Pt(0, element.bar.Dy() / 2))
|
||||
} else {
|
||||
return element.entity.Bounds().Min.
|
||||
Add(image.Pt(element.bar.Dx() / 2, 0))
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) scrollBy (delta int) {
|
||||
deltaPoint := image.Point { }
|
||||
if element.vertical {
|
||||
deltaPoint.Y = delta
|
||||
} else {
|
||||
deltaPoint.X = delta
|
||||
}
|
||||
if element.onScroll != nil {
|
||||
element.onScroll(element.viewportBounds.Min.Add(deltaPoint))
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) dragTo (point image.Point) {
|
||||
point = point.Sub(element.dragOffset)
|
||||
var scrollX, scrollY float64
|
||||
|
||||
if element.vertical {
|
||||
ratio :=
|
||||
float64(element.contentBounds.Dy()) /
|
||||
float64(element.track.Dy())
|
||||
scrollX = float64(element.viewportBounds.Min.X)
|
||||
scrollY = float64(point.Y) * ratio
|
||||
} else {
|
||||
ratio :=
|
||||
float64(element.contentBounds.Dx()) /
|
||||
float64(element.track.Dx())
|
||||
scrollX = float64(point.X) * ratio
|
||||
scrollY = float64(element.viewportBounds.Min.Y)
|
||||
}
|
||||
|
||||
if element.onScroll != nil {
|
||||
element.onScroll(image.Pt(int(scrollX), int(scrollY)))
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) recalculate () {
|
||||
if element.vertical {
|
||||
element.recalculateVertical()
|
||||
} else {
|
||||
element.recalculateHorizontal()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) recalculateVertical () {
|
||||
bounds := element.entity.Bounds()
|
||||
padding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
|
||||
element.track = padding.Apply(bounds)
|
||||
|
||||
contentBounds := element.contentBounds
|
||||
viewportBounds := element.viewportBounds
|
||||
if element.Enabled() {
|
||||
element.bar.Min.X = element.track.Min.X
|
||||
element.bar.Max.X = element.track.Max.X
|
||||
|
||||
ratio :=
|
||||
float64(element.track.Dy()) /
|
||||
float64(contentBounds.Dy())
|
||||
element.bar.Min.Y = int(float64(viewportBounds.Min.Y) * ratio)
|
||||
element.bar.Max.Y = int(float64(viewportBounds.Max.Y) * ratio)
|
||||
|
||||
element.bar.Min.Y += element.track.Min.Y
|
||||
element.bar.Max.Y += element.track.Min.Y
|
||||
}
|
||||
|
||||
// if the handle is out of bounds, don't display it
|
||||
if element.bar.Dy() >= element.track.Dy() {
|
||||
element.bar = image.Rectangle { }
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) recalculateHorizontal () {
|
||||
bounds := element.entity.Bounds()
|
||||
padding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
|
||||
element.track = padding.Apply(bounds)
|
||||
|
||||
contentBounds := element.contentBounds
|
||||
viewportBounds := element.viewportBounds
|
||||
if element.Enabled() {
|
||||
element.bar.Min.Y = element.track.Min.Y
|
||||
element.bar.Max.Y = element.track.Max.Y
|
||||
|
||||
ratio :=
|
||||
float64(element.track.Dx()) /
|
||||
float64(contentBounds.Dx())
|
||||
element.bar.Min.X = int(float64(viewportBounds.Min.X) * ratio)
|
||||
element.bar.Max.X = int(float64(viewportBounds.Max.X) * ratio)
|
||||
|
||||
element.bar.Min.X += element.track.Min.X
|
||||
element.bar.Max.X += element.track.Min.X
|
||||
}
|
||||
|
||||
// if the handle is out of bounds, don't display it
|
||||
if element.bar.Dx() >= element.track.Dx() {
|
||||
element.bar = image.Rectangle { }
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) updateMinimumSize () {
|
||||
gutterPadding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
|
||||
handlePadding := element.entity.Theme().Padding(tomo.PatternHandle, element.c)
|
||||
if element.vertical {
|
||||
element.entity.SetMinimumSize (
|
||||
gutterPadding.Horizontal() + handlePadding.Horizontal(),
|
||||
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
|
||||
} else {
|
||||
element.entity.SetMinimumSize (
|
||||
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
|
||||
gutterPadding.Vertical() + handlePadding.Vertical())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,260 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
|
||||
// Slider is a slider control with a floating point value between zero and one.
|
||||
type Slider struct {
|
||||
slider
|
||||
}
|
||||
|
||||
// NewVSlider creates a new horizontal slider with the specified value.
|
||||
func NewVSlider (value float64) (element *Slider) {
|
||||
element = NewHSlider(value)
|
||||
element.vertical = true
|
||||
return
|
||||
}
|
||||
|
||||
// NewHSlider creates a new horizontal slider with the specified value.
|
||||
func NewHSlider (value float64) (element *Slider) {
|
||||
element = &Slider { }
|
||||
element.value = value
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.construct()
|
||||
return
|
||||
}
|
||||
|
||||
type slider struct {
|
||||
entity tomo.Entity
|
||||
|
||||
c tomo.Case
|
||||
|
||||
value float64
|
||||
vertical bool
|
||||
dragging bool
|
||||
enabled bool
|
||||
dragOffset int
|
||||
track image.Rectangle
|
||||
bar image.Rectangle
|
||||
|
||||
onSlide func ()
|
||||
onRelease func ()
|
||||
}
|
||||
|
||||
func (element *slider) construct () {
|
||||
element.enabled = true
|
||||
if element.vertical {
|
||||
element.c = tomo.C("tomo", "sliderVertical")
|
||||
} else {
|
||||
element.c = tomo.C("tomo", "sliderHorizontal")
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *slider) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *slider) Draw (destination art.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
element.track = element.entity.Theme().Padding(tomo.PatternGutter, element.c).Apply(bounds)
|
||||
if element.vertical {
|
||||
barSize := element.track.Dx()
|
||||
element.bar = image.Rect(0, 0, barSize, barSize).Add(element.track.Min)
|
||||
barOffset :=
|
||||
float64(element.track.Dy() - barSize) *
|
||||
(1 - element.value)
|
||||
element.bar = element.bar.Add(image.Pt(0, int(barOffset)))
|
||||
} else {
|
||||
barSize := element.track.Dy()
|
||||
element.bar = image.Rect(0, 0, barSize, barSize).Add(element.track.Min)
|
||||
barOffset :=
|
||||
float64(element.track.Dx() - barSize) *
|
||||
element.value
|
||||
element.bar = element.bar.Add(image.Pt(int(barOffset), 0))
|
||||
}
|
||||
|
||||
state := tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
Pressed: element.dragging,
|
||||
}
|
||||
element.entity.Theme().Pattern(tomo.PatternGutter, state, element.c).Draw(destination, bounds)
|
||||
element.entity.Theme().Pattern(tomo.PatternHandle, state, element.c).Draw(destination, element.bar)
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *slider) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this slider can be dragged or not.
|
||||
func (element *slider) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this slider can be dragged or not.
|
||||
func (element *slider) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *slider) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *slider) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
if button == input.ButtonLeft {
|
||||
element.dragging = true
|
||||
element.value = element.valueFor(position.X, position.Y)
|
||||
if element.onSlide != nil {
|
||||
element.onSlide()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *slider) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button != input.ButtonLeft || !element.dragging { return }
|
||||
element.dragging = false
|
||||
if element.onRelease != nil {
|
||||
element.onRelease()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *slider) HandleMotion (position image.Point) {
|
||||
if element.dragging {
|
||||
element.dragging = true
|
||||
element.value = element.valueFor(position.X, position.Y)
|
||||
if element.onSlide != nil {
|
||||
element.onSlide()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *slider) HandleScroll (
|
||||
position image.Point,
|
||||
deltaX, deltaY float64,
|
||||
modifiers input.Modifiers,
|
||||
) { }
|
||||
|
||||
func (element *slider) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
switch key {
|
||||
case input.KeyUp:
|
||||
element.changeValue(0.1)
|
||||
case input.KeyDown:
|
||||
element.changeValue(-0.1)
|
||||
case input.KeyRight:
|
||||
if element.vertical {
|
||||
element.changeValue(-0.1)
|
||||
} else {
|
||||
element.changeValue(0.1)
|
||||
}
|
||||
case input.KeyLeft:
|
||||
if element.vertical {
|
||||
element.changeValue(0.1)
|
||||
} else {
|
||||
element.changeValue(-0.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *slider) HandleKeyUp (key input.Key, modifiers input.Modifiers) { }
|
||||
|
||||
// Value returns the slider's value.
|
||||
func (element *slider) Value () (value float64) {
|
||||
return element.value
|
||||
}
|
||||
|
||||
// SetValue sets the slider's value.
|
||||
func (element *slider) SetValue (value float64) {
|
||||
if value < 0 { value = 0 }
|
||||
if value > 1 { value = 1 }
|
||||
|
||||
if element.value == value { return }
|
||||
|
||||
element.value = value
|
||||
if element.onRelease != nil {
|
||||
element.onRelease()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// OnSlide sets a function to be called every time the slider handle changes
|
||||
// position while being dragged.
|
||||
func (element *slider) OnSlide (callback func ()) {
|
||||
element.onSlide = callback
|
||||
}
|
||||
|
||||
// OnRelease sets a function to be called when the handle stops being dragged.
|
||||
func (element *slider) OnRelease (callback func ()) {
|
||||
element.onRelease = callback
|
||||
}
|
||||
|
||||
func (element *slider) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
|
||||
func (element *slider) changeValue (delta float64) {
|
||||
element.value += delta
|
||||
if element.value < 0 {
|
||||
element.value = 0
|
||||
}
|
||||
if element.value > 1 {
|
||||
element.value = 1
|
||||
}
|
||||
if element.onRelease != nil {
|
||||
element.onRelease()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *slider) valueFor (x, y int) (value float64) {
|
||||
if element.vertical {
|
||||
value =
|
||||
float64(y - element.track.Min.Y - element.bar.Dy() / 2) /
|
||||
float64(element.track.Dy() - element.bar.Dy())
|
||||
value = 1 - value
|
||||
} else {
|
||||
value =
|
||||
float64(x - element.track.Min.X - element.bar.Dx() / 2) /
|
||||
float64(element.track.Dx() - element.bar.Dx())
|
||||
}
|
||||
|
||||
if value < 0 { value = 0 }
|
||||
if value > 1 { value = 1 }
|
||||
return
|
||||
}
|
||||
|
||||
func (element *slider) updateMinimumSize () {
|
||||
gutterPadding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
|
||||
handlePadding := element.entity.Theme().Padding(tomo.PatternHandle, element.c)
|
||||
if element.vertical {
|
||||
element.entity.SetMinimumSize (
|
||||
gutterPadding.Horizontal() + handlePadding.Horizontal(),
|
||||
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
|
||||
} else {
|
||||
element.entity.SetMinimumSize (
|
||||
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
|
||||
gutterPadding.Vertical() + handlePadding.Vertical())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package elements
|
||||
|
||||
import "tomo"
|
||||
import "art"
|
||||
|
||||
var spacerCase = tomo.C("tomo", "spacer")
|
||||
|
||||
// Spacer can be used to put space between two elements..
|
||||
type Spacer struct {
|
||||
entity tomo.Entity
|
||||
line bool
|
||||
}
|
||||
|
||||
// NewSpacer creates a new spacer.
|
||||
func NewSpacer () (element *Spacer) {
|
||||
element = &Spacer { }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
// NewLine creates a new line separator.
|
||||
func NewLine () (element *Spacer) {
|
||||
element = NewSpacer()
|
||||
element.SetLine(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Spacer) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Spacer) Draw (destination art.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
|
||||
if element.line {
|
||||
pattern := element.entity.Theme().Pattern (
|
||||
tomo.PatternLine,
|
||||
tomo.State { }, spacerCase)
|
||||
pattern.Draw(destination, bounds)
|
||||
} else {
|
||||
pattern := element.entity.Theme().Pattern (
|
||||
tomo.PatternBackground,
|
||||
tomo.State { }, spacerCase)
|
||||
pattern.Draw(destination, bounds)
|
||||
}
|
||||
}
|
||||
|
||||
/// SetLine sets whether or not the spacer will appear as a colored line.
|
||||
func (element *Spacer) SetLine (line bool) {
|
||||
if element.line == line { return }
|
||||
element.line = line
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Spacer) HandleThemeChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Spacer) updateMinimumSize () {
|
||||
if element.line {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternLine, spacerCase)
|
||||
element.entity.SetMinimumSize (
|
||||
padding.Horizontal(),
|
||||
padding.Vertical())
|
||||
} else {
|
||||
element.entity.SetMinimumSize(1, 1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
import "tomo/textdraw"
|
||||
|
||||
var switchCase = tomo.C("tomo", "switch")
|
||||
|
||||
// Switch is a toggle-able on/off switch with an optional label. It is
|
||||
// functionally identical to Checkbox, but plays a different semantic role.
|
||||
type Switch struct {
|
||||
entity tomo.Entity
|
||||
drawer textdraw.Drawer
|
||||
|
||||
enabled bool
|
||||
pressed bool
|
||||
checked bool
|
||||
text string
|
||||
|
||||
onToggle func ()
|
||||
}
|
||||
|
||||
// NewSwitch creates a new switch with the specified label text.
|
||||
func NewSwitch (text string, on bool) (element *Switch) {
|
||||
element = &Switch {
|
||||
checked: on,
|
||||
text: text,
|
||||
enabled: true,
|
||||
}
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal, switchCase))
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Switch) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Switch) Draw (destination art.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
|
||||
gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()).Add(bounds.Min)
|
||||
|
||||
state := tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
Pressed: element.pressed,
|
||||
On: element.checked,
|
||||
}
|
||||
|
||||
element.entity.DrawBackground(destination)
|
||||
|
||||
if element.checked {
|
||||
handleBounds.Min.X += bounds.Dy()
|
||||
handleBounds.Max.X += bounds.Dy()
|
||||
if element.pressed {
|
||||
handleBounds.Min.X -= 2
|
||||
handleBounds.Max.X -= 2
|
||||
}
|
||||
} else {
|
||||
if element.pressed {
|
||||
handleBounds.Min.X += 2
|
||||
handleBounds.Max.X += 2
|
||||
}
|
||||
}
|
||||
|
||||
gutterPattern := element.entity.Theme().Pattern (
|
||||
tomo.PatternGutter, state, switchCase)
|
||||
gutterPattern.Draw(destination, gutterBounds)
|
||||
|
||||
handlePattern := element.entity.Theme().Pattern (
|
||||
tomo.PatternHandle, state, switchCase)
|
||||
handlePattern.Draw(destination, handleBounds)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
offset := bounds.Min.Add(image.Point {
|
||||
X: bounds.Dy() * 2 +
|
||||
element.entity.Theme().Margin(tomo.PatternBackground, switchCase).X,
|
||||
})
|
||||
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
|
||||
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, switchCase)
|
||||
element.drawer.Draw(destination, foreground, offset)
|
||||
}
|
||||
|
||||
func (element *Switch) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Switch) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Switch) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button != input.ButtonLeft || !element.pressed { return }
|
||||
|
||||
element.pressed = false
|
||||
within := position.In(element.entity.Bounds())
|
||||
if within {
|
||||
element.checked = !element.checked
|
||||
}
|
||||
|
||||
element.entity.Invalidate()
|
||||
if within && element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Switch) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter {
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Switch) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
element.checked = !element.checked
|
||||
element.entity.Invalidate()
|
||||
if element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnToggle sets the function to be called when the switch is flipped.
|
||||
func (element *Switch) OnToggle (callback func ()) {
|
||||
element.onToggle = callback
|
||||
}
|
||||
|
||||
// Value reports whether or not the switch is currently on.
|
||||
func (element *Switch) Value () (on bool) {
|
||||
return element.checked
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *Switch) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this switch is enabled or not.
|
||||
func (element *Switch) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this switch can be toggled or not.
|
||||
func (element *Switch) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetText sets the checkbox's label text.
|
||||
func (element *Switch) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Switch) HandleThemeChange () {
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal, switchCase))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Switch) updateMinimumSize () {
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
lineHeight := element.drawer.LineHeight().Round()
|
||||
|
||||
if element.text == "" {
|
||||
element.entity.SetMinimumSize(lineHeight * 2, lineHeight)
|
||||
} else {
|
||||
element.entity.SetMinimumSize (
|
||||
lineHeight * 2 +
|
||||
element.entity.Theme().Margin(tomo.PatternBackground, switchCase).X +
|
||||
textBounds.Dx(),
|
||||
lineHeight)
|
||||
}
|
||||
}
|
|
@ -4,319 +4,206 @@ import "fmt"
|
|||
import "time"
|
||||
import "image"
|
||||
import "image/color"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/defaultfont"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
import "tomo"
|
||||
import "art"
|
||||
import "art/shatter"
|
||||
import "tomo/textdraw"
|
||||
import "art/shapes"
|
||||
import "art/artutil"
|
||||
import "art/patterns"
|
||||
|
||||
// Artist is an element that displays shapes and patterns drawn by the artist
|
||||
// Artist is an element that displays shapes and patterns drawn by the art
|
||||
// package in order to test it.
|
||||
type Artist struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
cellBounds image.Rectangle
|
||||
entity tomo.Entity
|
||||
}
|
||||
|
||||
// NewArtist creates a new artist test element.
|
||||
// NewArtist creates a new art test element.
|
||||
func NewArtist () (element *Artist) {
|
||||
element = &Artist { }
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
element.core.SetMinimumSize(480, 600)
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.entity.SetMinimumSize(240, 240)
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Artist) draw () {
|
||||
bounds := element.Bounds()
|
||||
element.cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5
|
||||
element.cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 8
|
||||
func (element *Artist) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
func (element *Artist) Draw (destination art.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
patterns.Uhex(0x000000FF).Draw(destination, bounds)
|
||||
|
||||
drawStart := time.Now()
|
||||
|
||||
// 0, 0
|
||||
artist.FillRectangle (
|
||||
element,
|
||||
artist.Beveled {
|
||||
artist.NewUniform(hex(0xFF0000FF)),
|
||||
artist.NewUniform(hex(0x0000FFFF)),
|
||||
},
|
||||
element.cellAt(0, 0))
|
||||
|
||||
// 1, 0
|
||||
artist.StrokeRectangle (
|
||||
element,
|
||||
artist.NewUniform(hex(0x00FF00FF)), 3,
|
||||
element.cellAt(1, 0))
|
||||
|
||||
// 2, 0
|
||||
artist.FillRectangle (
|
||||
element,
|
||||
artist.NewMultiBordered (
|
||||
artist.Stroke { Pattern: uhex(0xFF0000FF), Weight: 1 },
|
||||
artist.Stroke { Pattern: uhex(0x888800FF), Weight: 2 },
|
||||
artist.Stroke { Pattern: uhex(0x00FF00FF), Weight: 3 },
|
||||
artist.Stroke { Pattern: uhex(0x008888FF), Weight: 4 },
|
||||
artist.Stroke { Pattern: uhex(0x0000FFFF), Weight: 5 },
|
||||
),
|
||||
element.cellAt(2, 0))
|
||||
|
||||
// 3, 0
|
||||
artist.FillRectangle (
|
||||
element,
|
||||
artist.Bordered {
|
||||
Stroke: artist.Stroke { Pattern: uhex(0x0000FFFF), Weight: 5 },
|
||||
Fill: uhex(0xFF0000FF),
|
||||
},
|
||||
element.cellAt(3, 0))
|
||||
// 0, 0 - 3, 0
|
||||
for x := 0; x < 4; x ++ {
|
||||
element.colorLines(destination, x + 1, element.cellAt(destination, x, 0).Bounds())
|
||||
}
|
||||
|
||||
// 4, 0
|
||||
artist.FillRectangle (
|
||||
element,
|
||||
artist.Padded {
|
||||
Stroke: uhex(0xFFFFFFFF),
|
||||
Fill: uhex(0x666666FF),
|
||||
Sides: []int { 4, 13, 2, 0 },
|
||||
},
|
||||
element.cellAt(4, 0))
|
||||
c40 := element.cellAt(destination, 4, 0)
|
||||
shapes.StrokeColorRectangle(c40, artutil.Hex(0x888888FF), c40.Bounds(), 1)
|
||||
shapes.ColorLine (
|
||||
c40, artutil.Hex(0xFF0000FF), 1,
|
||||
c40.Bounds().Min, c40.Bounds().Max)
|
||||
|
||||
// 0, 1 - 3, 1
|
||||
for x := 0; x < 4; x ++ {
|
||||
artist.FillRectangle (
|
||||
element,
|
||||
artist.Striped {
|
||||
First: artist.Stroke { Pattern: uhex(0xFF8800FF), Weight: 7 },
|
||||
Second: artist.Stroke { Pattern: uhex(0x0088FFFF), Weight: 2 },
|
||||
Orientation: artist.Orientation(x),
|
||||
|
||||
},
|
||||
element.cellAt(x, 1))
|
||||
}
|
||||
// 0, 1
|
||||
c01 := element.cellAt(destination, 0, 1)
|
||||
shapes.StrokeColorRectangle(c01, artutil.Hex(0x888888FF), c01.Bounds(), 1)
|
||||
shapes.FillColorEllipse(destination, artutil.Hex(0x00FF00FF), c01.Bounds())
|
||||
|
||||
// 0, 2 - 3, 2
|
||||
for x := 0; x < 4; x ++ {
|
||||
element.lines(x + 1, element.cellAt(x, 2))
|
||||
}
|
||||
|
||||
// 0, 3
|
||||
artist.StrokeRectangle (
|
||||
element,uhex(0x888888FF), 1,
|
||||
element.cellAt(0, 3))
|
||||
artist.FillEllipse(element, uhex(0x00FF00FF), element.cellAt(0, 3))
|
||||
|
||||
// 1, 3 - 3, 3
|
||||
// 1, 1 - 3, 1
|
||||
for x := 1; x < 4; x ++ {
|
||||
artist.StrokeRectangle (
|
||||
element,uhex(0x888888FF), 1,
|
||||
element.cellAt(x, 3))
|
||||
artist.StrokeEllipse (
|
||||
element,
|
||||
[]artist.Pattern {
|
||||
uhex(0xFF0000FF),
|
||||
uhex(0x00FF00FF),
|
||||
uhex(0xFF00FFFF),
|
||||
c := element.cellAt(destination, x, 1)
|
||||
shapes.StrokeColorRectangle (
|
||||
destination, artutil.Hex(0x888888FF),
|
||||
c.Bounds(), 1)
|
||||
shapes.StrokeColorEllipse (
|
||||
destination,
|
||||
[]color.RGBA {
|
||||
artutil.Hex(0xFF0000FF),
|
||||
artutil.Hex(0x00FF00FF),
|
||||
artutil.Hex(0xFF00FFFF),
|
||||
} [x - 1],
|
||||
x, element.cellAt(x, 3))
|
||||
c.Bounds(), x)
|
||||
}
|
||||
|
||||
// 0, 4 - 3, 4
|
||||
for x := 0; x < 4; x ++ {
|
||||
artist.FillEllipse (
|
||||
element,
|
||||
artist.Split {
|
||||
First: uhex(0xFF0000FF),
|
||||
Second: uhex(0x0000FFFF),
|
||||
Orientation: artist.Orientation(x),
|
||||
},
|
||||
element.cellAt(x, 4))
|
||||
// 4, 1
|
||||
c41 := element.cellAt(destination, 4, 1)
|
||||
shatterPos := c41.Bounds().Min
|
||||
rocks := []image.Rectangle {
|
||||
image.Rect(3, 12, 13, 23).Add(shatterPos),
|
||||
// image.Rect(30, 10, 40, 23).Add(shatterPos),
|
||||
image.Rect(55, 40, 70, 49).Add(shatterPos),
|
||||
image.Rect(30, -10, 40, 43).Add(shatterPos),
|
||||
image.Rect(80, 30, 90, 45).Add(shatterPos),
|
||||
}
|
||||
tiles := shatter.Shatter(c41.Bounds(), rocks...)
|
||||
for index, tile := range tiles {
|
||||
[]art.Pattern {
|
||||
patterns.Uhex(0xFF0000FF),
|
||||
patterns.Uhex(0x00FF00FF),
|
||||
patterns.Uhex(0xFF00FFFF),
|
||||
patterns.Uhex(0xFFFF00FF),
|
||||
patterns.Uhex(0x00FFFFFF),
|
||||
} [index % 5].Draw(destination, tile)
|
||||
}
|
||||
|
||||
// 0, 2
|
||||
c02 := element.cellAt(destination, 0, 2)
|
||||
shapes.StrokeColorRectangle(c02, artutil.Hex(0x888888FF), c02.Bounds(), 1)
|
||||
shapes.FillEllipse(c02, c41, c02.Bounds())
|
||||
|
||||
// 1, 2
|
||||
c12 := element.cellAt(destination, 1, 2)
|
||||
shapes.StrokeColorRectangle(c12, artutil.Hex(0x888888FF), c12.Bounds(), 1)
|
||||
shapes.StrokeEllipse(c12, c41, c12.Bounds(), 5)
|
||||
|
||||
// 2, 2
|
||||
c22 := element.cellAt(destination, 2, 2)
|
||||
shapes.FillRectangle(c22, c41, c22.Bounds())
|
||||
|
||||
// 3, 2
|
||||
c32 := element.cellAt(destination, 3, 2)
|
||||
shapes.StrokeRectangle(c32, c41, c32.Bounds(), 5)
|
||||
|
||||
// 4, 2
|
||||
c42 := element.cellAt(destination, 4, 2)
|
||||
|
||||
// 0, 3
|
||||
c03 := element.cellAt(destination, 0, 3)
|
||||
patterns.Border {
|
||||
Canvas: element.thingy(c42),
|
||||
Inset: art.Inset { 8, 8, 8, 8 },
|
||||
}.Draw(c03, c03.Bounds())
|
||||
|
||||
// 1, 3
|
||||
c13 := element.cellAt(destination, 1, 3)
|
||||
patterns.Border {
|
||||
Canvas: element.thingy(c42),
|
||||
Inset: art.Inset { 8, 8, 8, 8 },
|
||||
}.Draw(c13, c13.Bounds().Inset(10))
|
||||
|
||||
// 2, 3
|
||||
c23 := element.cellAt(destination, 2, 3)
|
||||
patterns.Border {
|
||||
Canvas: element.thingy(c42),
|
||||
Inset: art.Inset { 8, 8, 8, 8 },
|
||||
}.Draw(c23, c23.Bounds())
|
||||
patterns.Border {
|
||||
Canvas: element.thingy(c42),
|
||||
Inset: art.Inset { 8, 8, 8, 8 },
|
||||
}.Draw(art.Cut(c23, c23.Bounds().Inset(16)), c23.Bounds())
|
||||
|
||||
// how long did that take to render?
|
||||
drawTime := time.Since(drawStart)
|
||||
textDrawer := artist.TextDrawer { }
|
||||
textDrawer.SetFace(defaultfont.FaceRegular)
|
||||
textDrawer := textdraw.Drawer { }
|
||||
textDrawer.SetFace(element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
tomo.C("tomo", "art")))
|
||||
textDrawer.SetText ([]rune (fmt.Sprintf (
|
||||
"%dms\n%dus",
|
||||
drawTime.Milliseconds(),
|
||||
drawTime.Microseconds())))
|
||||
textDrawer.Draw(element, uhex(0xFFFFFFFF), image.Pt(8, bounds.Max.Y - 24))
|
||||
|
||||
// 0, 5
|
||||
artist.FillRectangle (
|
||||
element,
|
||||
artist.QuadBeveled {
|
||||
uhex(0x880000FF),
|
||||
uhex(0x00FF00FF),
|
||||
uhex(0x0000FFFF),
|
||||
uhex(0xFF00FFFF),
|
||||
},
|
||||
element.cellAt(0, 5))
|
||||
|
||||
// 1, 5
|
||||
artist.FillRectangle (
|
||||
element,
|
||||
artist.Checkered {
|
||||
First: artist.QuadBeveled {
|
||||
uhex(0x880000FF),
|
||||
uhex(0x00FF00FF),
|
||||
uhex(0x0000FFFF),
|
||||
uhex(0xFF00FFFF),
|
||||
},
|
||||
Second: artist.Striped {
|
||||
First: artist.Stroke { Pattern: uhex(0xFF8800FF), Weight: 1 },
|
||||
Second: artist.Stroke { Pattern: uhex(0x0088FFFF), Weight: 1 },
|
||||
Orientation: artist.OrientationVertical,
|
||||
},
|
||||
CellWidth: 32,
|
||||
CellHeight: 16,
|
||||
},
|
||||
element.cellAt(1, 5))
|
||||
|
||||
// 2, 5
|
||||
artist.FillRectangle (
|
||||
element,
|
||||
artist.Dotted {
|
||||
Foreground: uhex(0x00FF00FF),
|
||||
Background: artist.Checkered {
|
||||
First: uhex(0x444444FF),
|
||||
Second: uhex(0x888888FF),
|
||||
CellWidth: 16,
|
||||
CellHeight: 16,
|
||||
},
|
||||
Size: 8,
|
||||
Spacing: 16,
|
||||
},
|
||||
element.cellAt(2, 5))
|
||||
|
||||
// 3, 5
|
||||
artist.FillRectangle (
|
||||
element,
|
||||
artist.Tiled {
|
||||
Pattern: artist.QuadBeveled {
|
||||
uhex(0x880000FF),
|
||||
uhex(0x00FF00FF),
|
||||
uhex(0x0000FFFF),
|
||||
uhex(0xFF00FFFF),
|
||||
},
|
||||
CellWidth: 17,
|
||||
CellHeight: 23,
|
||||
},
|
||||
element.cellAt(3, 5))
|
||||
|
||||
// 0, 6 - 3, 6
|
||||
for x := 0; x < 4; x ++ {
|
||||
artist.FillRectangle (
|
||||
element,
|
||||
artist.Gradient {
|
||||
First: uhex(0xFF0000FF),
|
||||
Second: uhex(0x0000FFFF),
|
||||
Orientation: artist.Orientation(x),
|
||||
},
|
||||
element.cellAt(x, 6))
|
||||
}
|
||||
|
||||
// 0, 7
|
||||
artist.FillEllipse (
|
||||
element,
|
||||
artist.EllipticallyBordered {
|
||||
Fill: artist.Gradient {
|
||||
First: uhex(0x00FF00FF),
|
||||
Second: uhex(0x0000FFFF),
|
||||
Orientation: artist.OrientationVertical,
|
||||
},
|
||||
Stroke: artist.Stroke { Pattern: uhex(0x00FF00), Weight: 5 },
|
||||
},
|
||||
element.cellAt(0, 7))
|
||||
|
||||
// 1, 7
|
||||
artist.FillRectangle (
|
||||
element,
|
||||
artist.Noisy {
|
||||
Low: uhex(0x000000FF),
|
||||
High: uhex(0xFFFFFFFF),
|
||||
Seed: 0,
|
||||
},
|
||||
element.cellAt(1, 7),
|
||||
)
|
||||
|
||||
// 2, 7
|
||||
artist.FillRectangle (
|
||||
element,
|
||||
artist.Noisy {
|
||||
Low: uhex(0x000000FF),
|
||||
High: artist.Gradient {
|
||||
First: uhex(0x000000FF),
|
||||
Second: uhex(0xFFFFFFFF),
|
||||
Orientation: artist.OrientationVertical,
|
||||
},
|
||||
Seed: 0,
|
||||
},
|
||||
element.cellAt(2, 7),
|
||||
)
|
||||
|
||||
// 3, 7
|
||||
artist.FillRectangle (
|
||||
element,
|
||||
artist.Noisy {
|
||||
Low: uhex(0x000000FF),
|
||||
High: uhex(0xFFFFFFFF),
|
||||
Seed: 0,
|
||||
Harsh: true,
|
||||
},
|
||||
element.cellAt(3, 7),
|
||||
)
|
||||
textDrawer.Draw (
|
||||
destination, artutil.Hex(0xFFFFFFFF),
|
||||
image.Pt(bounds.Min.X + 8, bounds.Max.Y - 24))
|
||||
}
|
||||
|
||||
func (element *Artist) lines (weight int, bounds image.Rectangle) {
|
||||
func (element *Artist) colorLines (destination art.Canvas, weight int, bounds image.Rectangle) {
|
||||
bounds = bounds.Inset(4)
|
||||
c := uhex(0xFFFFFFFF)
|
||||
artist.Line(element, c, weight, bounds.Min, bounds.Max)
|
||||
artist.Line (
|
||||
element, c, weight,
|
||||
c := artutil.Hex(0xFFFFFFFF)
|
||||
shapes.ColorLine(destination, c, weight, bounds.Min, bounds.Max)
|
||||
shapes.ColorLine (
|
||||
destination, c, weight,
|
||||
image.Pt(bounds.Max.X, bounds.Min.Y),
|
||||
image.Pt(bounds.Min.X, bounds.Max.Y))
|
||||
artist.Line (
|
||||
element, c, weight,
|
||||
shapes.ColorLine (
|
||||
destination, c, weight,
|
||||
image.Pt(bounds.Max.X, bounds.Min.Y + 16),
|
||||
image.Pt(bounds.Min.X, bounds.Max.Y - 16))
|
||||
artist.Line (
|
||||
element, c, weight,
|
||||
shapes.ColorLine (
|
||||
destination, c, weight,
|
||||
image.Pt(bounds.Min.X, bounds.Min.Y + 16),
|
||||
image.Pt(bounds.Max.X, bounds.Max.Y - 16))
|
||||
artist.Line (
|
||||
element, c, weight,
|
||||
shapes.ColorLine (
|
||||
destination, c, weight,
|
||||
image.Pt(bounds.Min.X + 20, bounds.Min.Y),
|
||||
image.Pt(bounds.Max.X - 20, bounds.Max.Y))
|
||||
artist.Line (
|
||||
element, c, weight,
|
||||
shapes.ColorLine (
|
||||
destination, c, weight,
|
||||
image.Pt(bounds.Max.X - 20, bounds.Min.Y),
|
||||
image.Pt(bounds.Min.X + 20, bounds.Max.Y))
|
||||
artist.Line (
|
||||
element, c, weight,
|
||||
shapes.ColorLine (
|
||||
destination, c, weight,
|
||||
image.Pt(bounds.Min.X, bounds.Min.Y + bounds.Dy() / 2),
|
||||
image.Pt(bounds.Max.X, bounds.Min.Y + bounds.Dy() / 2))
|
||||
artist.Line (
|
||||
element, c, weight,
|
||||
shapes.ColorLine (
|
||||
destination, c, weight,
|
||||
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Min.Y),
|
||||
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Max.Y))
|
||||
}
|
||||
|
||||
func (element *Artist) cellAt (x, y int) (image.Rectangle) {
|
||||
return element.cellBounds.Add (image.Pt (
|
||||
x * element.cellBounds.Dx(),
|
||||
y * element.cellBounds.Dy()))
|
||||
func (element *Artist) cellAt (destination art.Canvas, x, y int) (art.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
cellBounds := image.Rectangle { }
|
||||
cellBounds.Min = bounds.Min
|
||||
cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5
|
||||
cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 4
|
||||
return art.Cut (destination, cellBounds.Add (image.Pt (
|
||||
x * cellBounds.Dx(),
|
||||
y * cellBounds.Dy())))
|
||||
}
|
||||
|
||||
func hex (n uint32) (c color.RGBA) {
|
||||
c.A = uint8(n)
|
||||
c.B = uint8(n >> 8)
|
||||
c.G = uint8(n >> 16)
|
||||
c.R = uint8(n >> 24)
|
||||
return
|
||||
}
|
||||
|
||||
func uhex (n uint32) (artist.Pattern) {
|
||||
return artist.NewUniform (color.RGBA {
|
||||
A: uint8(n),
|
||||
B: uint8(n >> 8),
|
||||
G: uint8(n >> 16),
|
||||
R: uint8(n >> 24),
|
||||
})
|
||||
func (element *Artist) thingy (destination art.Canvas) (result art.Canvas) {
|
||||
bounds := destination.Bounds()
|
||||
bounds = image.Rect(0, 0, 32, 32).Add(bounds.Min)
|
||||
shapes.FillColorRectangle(destination, artutil.Hex(0x440000FF), bounds)
|
||||
shapes.StrokeColorRectangle(destination, artutil.Hex(0xFF0000FF), bounds, 1)
|
||||
shapes.StrokeColorRectangle(destination, artutil.Hex(0x004400FF), bounds.Inset(4), 1)
|
||||
shapes.FillColorRectangle(destination, artutil.Hex(0x004444FF), bounds.Inset(12))
|
||||
shapes.StrokeColorRectangle(destination, artutil.Hex(0x888888FF), bounds.Inset(8), 1)
|
||||
return art.Cut(destination, bounds)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
// Package testing provides elements that are used to test different parts of
|
||||
// Tomo's API.
|
||||
package testing
|
|
@ -1,70 +1,86 @@
|
|||
package testing
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
import "tomo"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
import "art/shapes"
|
||||
import "art/artutil"
|
||||
|
||||
var mouseCase = tomo.C("tomo", "mouse")
|
||||
|
||||
// Mouse is an element capable of testing mouse input. When the mouse is clicked
|
||||
// and dragged on it, it draws a trail.
|
||||
type Mouse struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
drawing bool
|
||||
color artist.Pattern
|
||||
entity tomo.Entity
|
||||
pressed bool
|
||||
lastMousePos image.Point
|
||||
}
|
||||
|
||||
// NewMouse creates a new mouse test element.
|
||||
func NewMouse () (element *Mouse) {
|
||||
element = &Mouse { }
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
element.core.SetMinimumSize(32, 32)
|
||||
element.color = artist.NewUniform(color.Black)
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.entity.SetMinimumSize(32, 32)
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Mouse) draw () {
|
||||
bounds := element.Bounds()
|
||||
pattern, _ := theme.AccentPattern(theme.PatternState { })
|
||||
artist.FillRectangle(element, pattern, bounds)
|
||||
artist.StrokeRectangle (
|
||||
element,
|
||||
artist.NewUniform(color.Black), 1,
|
||||
bounds)
|
||||
artist.Line (
|
||||
element, artist.NewUniform(color.White), 1,
|
||||
func (element *Mouse) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
func (element *Mouse) Draw (destination art.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
accent := element.entity.Theme().Color (
|
||||
tomo.ColorAccent,
|
||||
tomo.State { },
|
||||
mouseCase)
|
||||
shapes.FillColorRectangle(destination, accent, bounds)
|
||||
shapes.StrokeColorRectangle (
|
||||
destination,
|
||||
artutil.Hex(0x000000FF),
|
||||
bounds, 1)
|
||||
shapes.ColorLine (
|
||||
destination, artutil.Hex(0xFFFFFFFF), 1,
|
||||
bounds.Min.Add(image.Pt(1, 1)),
|
||||
bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2)))
|
||||
artist.Line (
|
||||
element, artist.NewUniform(color.White), 1,
|
||||
shapes.ColorLine (
|
||||
destination, artutil.Hex(0xFFFFFFFF), 1,
|
||||
bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)),
|
||||
bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1)))
|
||||
if element.pressed {
|
||||
midpoint := bounds.Min.Add(bounds.Max.Sub(bounds.Min).Div(2))
|
||||
shapes.ColorLine (
|
||||
destination, artutil.Hex(0x000000FF), 1,
|
||||
midpoint, element.lastMousePos)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Mouse) HandleMouseDown (x, y int, button tomo.Button) {
|
||||
element.drawing = true
|
||||
element.lastMousePos = image.Pt(x, y)
|
||||
func (element *Mouse) HandleThemeChange (new tomo.Theme) {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Mouse) HandleMouseUp (x, y int, button tomo.Button) {
|
||||
element.drawing = false
|
||||
mousePos := image.Pt(x, y)
|
||||
element.core.DamageRegion (artist.Line (
|
||||
element, element.color, 1,
|
||||
element.lastMousePos, mousePos))
|
||||
element.lastMousePos = mousePos
|
||||
func (element *Mouse) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
element.pressed = true
|
||||
element.lastMousePos = position
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Mouse) HandleMouseMove (x, y int) {
|
||||
if !element.drawing { return }
|
||||
mousePos := image.Pt(x, y)
|
||||
element.core.DamageRegion (artist.Line (
|
||||
element, element.color, 1,
|
||||
element.lastMousePos, mousePos))
|
||||
element.lastMousePos = mousePos
|
||||
func (element *Mouse) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
element.pressed = false
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Mouse) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||
func (element *Mouse) HandleMotion (position image.Point) {
|
||||
if !element.pressed { return }
|
||||
element.lastMousePos = position
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,576 @@
|
|||
package elements
|
||||
|
||||
import "io"
|
||||
import "time"
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "tomo/data"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
import "tomo/textdraw"
|
||||
import "tomo/textmanip"
|
||||
import "tomo/fixedutil"
|
||||
import "art/shapes"
|
||||
|
||||
var textBoxCase = tomo.C("tomo", "textBox")
|
||||
|
||||
// TextBox is a single-line text input.
|
||||
type TextBox struct {
|
||||
entity tomo.Entity
|
||||
|
||||
enabled bool
|
||||
lastClick time.Time
|
||||
dragging int
|
||||
dot textmanip.Dot
|
||||
scroll int
|
||||
placeholder string
|
||||
text []rune
|
||||
|
||||
placeholderDrawer textdraw.Drawer
|
||||
valueDrawer textdraw.Drawer
|
||||
|
||||
onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool)
|
||||
onChange func ()
|
||||
onEnter func ()
|
||||
onScrollBoundsChange func ()
|
||||
}
|
||||
|
||||
// NewTextBox creates a new text box with the specified placeholder text, and
|
||||
// a value. When the value is empty, the placeholder will be displayed in gray
|
||||
// text.
|
||||
func NewTextBox (placeholder, value string) (element *TextBox) {
|
||||
element = &TextBox { enabled: true }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.placeholder = placeholder
|
||||
element.placeholderDrawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal, textBoxCase))
|
||||
element.valueDrawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal, textBoxCase))
|
||||
element.placeholderDrawer.SetText([]rune(placeholder))
|
||||
element.updateMinimumSize()
|
||||
element.SetValue(value)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *TextBox) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *TextBox) Draw (destination art.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
|
||||
state := element.state()
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternInput, state, textBoxCase)
|
||||
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
|
||||
innerCanvas := art.Cut(destination, padding.Apply(bounds))
|
||||
pattern.Draw(destination, bounds)
|
||||
offset := element.textOffset()
|
||||
|
||||
if element.entity.Focused() && !element.dot.Empty() {
|
||||
// draw selection bounds
|
||||
accent := element.entity.Theme().Color(tomo.ColorAccent, state, textBoxCase)
|
||||
canon := element.dot.Canon()
|
||||
foff := fixedutil.Pt(offset)
|
||||
start := element.valueDrawer.PositionAt(canon.Start).Add(foff)
|
||||
end := element.valueDrawer.PositionAt(canon.End).Add(foff)
|
||||
end.Y += element.valueDrawer.LineHeight()
|
||||
shapes.FillColorRectangle (
|
||||
innerCanvas,
|
||||
accent,
|
||||
image.Rectangle {
|
||||
fixedutil.RoundPt(start),
|
||||
fixedutil.RoundPt(end),
|
||||
})
|
||||
}
|
||||
|
||||
if len(element.text) == 0 {
|
||||
// draw placeholder
|
||||
textBounds := element.placeholderDrawer.LayoutBounds()
|
||||
foreground := element.entity.Theme().Color (
|
||||
tomo.ColorForeground,
|
||||
tomo.State { Disabled: true }, textBoxCase)
|
||||
element.placeholderDrawer.Draw (
|
||||
innerCanvas,
|
||||
foreground,
|
||||
offset.Sub(textBounds.Min))
|
||||
} else {
|
||||
// draw input value
|
||||
textBounds := element.valueDrawer.LayoutBounds()
|
||||
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, textBoxCase)
|
||||
element.valueDrawer.Draw (
|
||||
innerCanvas,
|
||||
foreground,
|
||||
offset.Sub(textBounds.Min))
|
||||
}
|
||||
|
||||
if element.entity.Focused() && element.dot.Empty() {
|
||||
// draw cursor
|
||||
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, textBoxCase)
|
||||
cursorPosition := fixedutil.RoundPt (
|
||||
element.valueDrawer.PositionAt(element.dot.End))
|
||||
shapes.ColorLine (
|
||||
innerCanvas,
|
||||
foreground, 1,
|
||||
cursorPosition.Add(offset),
|
||||
image.Pt (
|
||||
cursorPosition.X,
|
||||
cursorPosition.Y + element.valueDrawer.
|
||||
LineHeight().Round()).Add(offset))
|
||||
}
|
||||
}
|
||||
|
||||
// Layout causes the element to perform a layout operation.
|
||||
func (element *TextBox) Layout () {
|
||||
element.scrollToCursor()
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
|
||||
switch button {
|
||||
case input.ButtonLeft:
|
||||
runeIndex := element.atPosition(position)
|
||||
if runeIndex == -1 { return }
|
||||
|
||||
if time.Since(element.lastClick) < element.entity.Config().DoubleClickDelay() {
|
||||
element.dragging = 2
|
||||
element.dot = textmanip.WordAround(element.text, runeIndex)
|
||||
} else {
|
||||
element.dragging = 1
|
||||
element.dot = textmanip.EmptyDot(runeIndex)
|
||||
element.lastClick = time.Now()
|
||||
}
|
||||
|
||||
element.entity.Invalidate()
|
||||
case input.ButtonRight:
|
||||
element.contextMenu(position)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button == input.ButtonLeft {
|
||||
element.dragging = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleMotion (position image.Point) {
|
||||
if !element.Enabled() { return }
|
||||
|
||||
switch element.dragging {
|
||||
case 1:
|
||||
runeIndex := element.atPosition(position)
|
||||
if runeIndex > -1 {
|
||||
element.dot.End = runeIndex
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
case 2:
|
||||
runeIndex := element.atPosition(position)
|
||||
if runeIndex > -1 {
|
||||
if runeIndex < element.dot.Start {
|
||||
element.dot.End =
|
||||
runeIndex -
|
||||
textmanip.WordToLeft (
|
||||
element.text,
|
||||
runeIndex)
|
||||
} else {
|
||||
element.dot.End =
|
||||
runeIndex +
|
||||
textmanip.WordToRight (
|
||||
element.text,
|
||||
runeIndex)
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) textOffset () image.Point {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
|
||||
bounds := element.entity.Bounds()
|
||||
innerBounds := padding.Apply(bounds)
|
||||
textHeight := element.valueDrawer.LineHeight().Round()
|
||||
return bounds.Min.Add (image.Pt (
|
||||
padding[art.SideLeft] - element.scroll,
|
||||
padding[art.SideTop] + (innerBounds.Dy() - textHeight) / 2))
|
||||
}
|
||||
|
||||
func (element *TextBox) atPosition (position image.Point) int {
|
||||
offset := element.textOffset()
|
||||
textBoundsMin := element.valueDrawer.LayoutBounds().Min
|
||||
return element.valueDrawer.AtPosition (
|
||||
fixedutil.Pt(position.Sub(offset).Add(textBoundsMin)))
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) {
|
||||
if element.onKeyDown != nil && element.onKeyDown(key, modifiers) {
|
||||
return
|
||||
}
|
||||
|
||||
scrollMemory := element.scroll
|
||||
textChanged := false
|
||||
switch {
|
||||
case key == input.KeyEnter:
|
||||
if element.onEnter != nil {
|
||||
element.onEnter()
|
||||
}
|
||||
|
||||
case key == input.KeyBackspace:
|
||||
if len(element.text) < 1 { break }
|
||||
element.text, element.dot = textmanip.Backspace (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
textChanged = true
|
||||
|
||||
case key == input.KeyDelete:
|
||||
if len(element.text) < 1 { break }
|
||||
element.text, element.dot = textmanip.Delete (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
textChanged = true
|
||||
|
||||
case key == input.KeyLeft:
|
||||
if modifiers.Shift {
|
||||
element.dot = textmanip.SelectLeft (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
} else {
|
||||
element.dot = textmanip.MoveLeft (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
}
|
||||
element.scrollToCursor()
|
||||
element.entity.Invalidate()
|
||||
|
||||
case key == input.KeyRight:
|
||||
if modifiers.Shift {
|
||||
element.dot = textmanip.SelectRight (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
} else {
|
||||
element.dot = textmanip.MoveRight (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
}
|
||||
element.scrollToCursor()
|
||||
element.entity.Invalidate()
|
||||
|
||||
case key == 'a' && modifiers.Control:
|
||||
element.dot.Start = 0
|
||||
element.dot.End = len(element.text)
|
||||
element.scrollToCursor()
|
||||
element.entity.Invalidate()
|
||||
|
||||
case key == 'x' && modifiers.Control: element.Cut()
|
||||
case key == 'c' && modifiers.Control: element.Copy()
|
||||
case key == 'v' && modifiers.Control: element.Paste()
|
||||
|
||||
case key == input.KeyMenu:
|
||||
pos := fixedutil.RoundPt(element.valueDrawer.PositionAt(element.dot.End)).
|
||||
Add(element.textOffset())
|
||||
pos.Y += element.valueDrawer.LineHeight().Round()
|
||||
element.contextMenu(pos)
|
||||
|
||||
case key.Printable():
|
||||
element.text, element.dot = textmanip.Type (
|
||||
element.text,
|
||||
element.dot,
|
||||
rune(key))
|
||||
textChanged = true
|
||||
}
|
||||
|
||||
if textChanged {
|
||||
element.runOnChange()
|
||||
element.valueDrawer.SetText(element.text)
|
||||
element.scrollToCursor()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
if (textChanged || scrollMemory != element.scroll) {
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Cut cuts the selected text in the text box and places it in the clipboard.
|
||||
func (element *TextBox) Cut () {
|
||||
var lifted []rune
|
||||
element.text, element.dot, lifted = textmanip.Lift (
|
||||
element.text,
|
||||
element.dot)
|
||||
if lifted != nil {
|
||||
element.clipboardPut(lifted)
|
||||
element.notifyAsyncTextChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Copy copies the selected text in the text box and places it in the clipboard.
|
||||
func (element *TextBox) Copy () {
|
||||
element.clipboardPut(element.dot.Slice(element.text))
|
||||
}
|
||||
|
||||
// Paste pastes text data from the clipboard into the text box.
|
||||
func (element *TextBox) Paste () {
|
||||
window := element.entity.Window()
|
||||
if window == nil { return }
|
||||
window.Paste (func (d data.Data, err error) {
|
||||
if err != nil { return }
|
||||
reader, ok := d[data.MimePlain]
|
||||
if !ok { return }
|
||||
bytes, _ := io.ReadAll(reader)
|
||||
element.text, element.dot = textmanip.Type (
|
||||
element.text,
|
||||
element.dot,
|
||||
[]rune(string(bytes))...)
|
||||
element.notifyAsyncTextChange()
|
||||
})
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
|
||||
|
||||
// SetPlaceholder sets the element's placeholder text.
|
||||
func (element *TextBox) SetPlaceholder (placeholder string) {
|
||||
if element.placeholder == placeholder { return }
|
||||
|
||||
element.placeholder = placeholder
|
||||
element.placeholderDrawer.SetText([]rune(placeholder))
|
||||
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetValue sets the input's value.
|
||||
func (element *TextBox) SetValue (text string) {
|
||||
// if element.text == text { return }
|
||||
|
||||
element.text = []rune(text)
|
||||
element.runOnChange()
|
||||
element.valueDrawer.SetText(element.text)
|
||||
if element.dot.End > element.valueDrawer.Length() {
|
||||
element.dot = textmanip.EmptyDot(element.valueDrawer.Length())
|
||||
}
|
||||
element.scrollToCursor()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// Value returns the input's value.
|
||||
func (element *TextBox) Value () (value string) {
|
||||
return string(element.text)
|
||||
}
|
||||
|
||||
// Filled returns whether or not this element has a value.
|
||||
func (element *TextBox) Filled () (filled bool) {
|
||||
return len(element.text) > 0
|
||||
}
|
||||
|
||||
// OnKeyDown specifies a function to be called when a key is pressed within the
|
||||
// text input.
|
||||
func (element *TextBox) OnKeyDown (
|
||||
callback func (key input.Key, modifiers input.Modifiers) (handled bool),
|
||||
) {
|
||||
element.onKeyDown = callback
|
||||
}
|
||||
|
||||
// OnEnter specifies a function to be called when the enter key is pressed
|
||||
// within this input.
|
||||
func (element *TextBox) OnEnter (callback func ()) {
|
||||
element.onEnter = callback
|
||||
}
|
||||
|
||||
// OnChange specifies a function to be called when the value of this input
|
||||
// changes.
|
||||
func (element *TextBox) OnChange (callback func ()) {
|
||||
element.onChange = callback
|
||||
}
|
||||
|
||||
// OnScrollBoundsChange sets a function to be called when the element's viewport
|
||||
// bounds, content bounds, or scroll axes change.
|
||||
func (element *TextBox) OnScrollBoundsChange (callback func ()) {
|
||||
element.onScrollBoundsChange = callback
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *TextBox) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this label can be edited or not.
|
||||
func (element *TextBox) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this label can be edited or not.
|
||||
func (element *TextBox) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// ScrollContentBounds returns the full content size of the element.
|
||||
func (element *TextBox) ScrollContentBounds () (bounds image.Rectangle) {
|
||||
bounds = element.valueDrawer.LayoutBounds()
|
||||
return bounds.Sub(bounds.Min)
|
||||
}
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's viewport
|
||||
// relative to ScrollBounds.
|
||||
func (element *TextBox) ScrollViewportBounds () (bounds image.Rectangle) {
|
||||
return image.Rect (
|
||||
element.scroll,
|
||||
0,
|
||||
element.scroll + element.scrollViewportWidth(),
|
||||
0)
|
||||
}
|
||||
|
||||
// ScrollTo scrolls the viewport to the specified point relative to
|
||||
// ScrollBounds.
|
||||
func (element *TextBox) ScrollTo (position image.Point) {
|
||||
// constrain to minimum
|
||||
element.scroll = position.X
|
||||
if element.scroll < 0 { element.scroll = 0 }
|
||||
|
||||
// constrain to maximum
|
||||
contentBounds := element.ScrollContentBounds()
|
||||
maxPosition := contentBounds.Max.X - element.scrollViewportWidth()
|
||||
if element.scroll > maxPosition { element.scroll = maxPosition }
|
||||
|
||||
element.entity.Invalidate()
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
}
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
func (element *TextBox) ScrollAxes () (horizontal, vertical bool) {
|
||||
return true, false
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleThemeChange () {
|
||||
face := element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
textBoxCase)
|
||||
element.placeholderDrawer.SetFace(face)
|
||||
element.valueDrawer.SetFace(face)
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *TextBox) contextMenu (position image.Point) {
|
||||
window := element.entity.Window()
|
||||
menu, err := window.NewMenu(image.Rectangle { position, position })
|
||||
if err != nil { return }
|
||||
|
||||
closeAnd := func (callback func ()) func () {
|
||||
return func () { callback(); menu.Close() }
|
||||
}
|
||||
|
||||
cutButton := NewButton("Cut")
|
||||
cutButton.ShowText(false)
|
||||
cutButton.SetIcon(tomo.IconCut)
|
||||
cutButton.SetEnabled(!element.dot.Empty())
|
||||
cutButton.OnClick(closeAnd(element.Cut))
|
||||
|
||||
copyButton := NewButton("Copy")
|
||||
copyButton.ShowText(false)
|
||||
copyButton.SetIcon(tomo.IconCopy)
|
||||
copyButton.SetEnabled(!element.dot.Empty())
|
||||
copyButton.OnClick(closeAnd(element.Copy))
|
||||
|
||||
pasteButton := NewButton("Paste")
|
||||
pasteButton.ShowText(false)
|
||||
pasteButton.SetIcon(tomo.IconPaste)
|
||||
pasteButton.OnClick(closeAnd(element.Paste))
|
||||
|
||||
menu.Adopt (NewHBox (
|
||||
SpaceNone,
|
||||
pasteButton,
|
||||
copyButton,
|
||||
cutButton,
|
||||
))
|
||||
pasteButton.Focus()
|
||||
menu.Show()
|
||||
}
|
||||
|
||||
func (element *TextBox) runOnChange () {
|
||||
if element.onChange != nil {
|
||||
element.onChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) scrollViewportWidth () (width int) {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
|
||||
return padding.Apply(element.entity.Bounds()).Dx()
|
||||
}
|
||||
|
||||
func (element *TextBox) scrollToCursor () {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
|
||||
bounds := padding.Apply(element.entity.Bounds())
|
||||
bounds = bounds.Sub(bounds.Min)
|
||||
bounds.Max.X -= element.valueDrawer.Em().Round()
|
||||
cursorPosition := fixedutil.RoundPt (
|
||||
element.valueDrawer.PositionAt(element.dot.End))
|
||||
cursorPosition.X -= element.scroll
|
||||
maxX := bounds.Max.X
|
||||
minX := maxX
|
||||
if cursorPosition.X > maxX {
|
||||
element.scroll += cursorPosition.X - maxX
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
element.entity.Invalidate()
|
||||
} else if cursorPosition.X < minX {
|
||||
element.scroll -= minX - cursorPosition.X
|
||||
if element.scroll < 0 { element.scroll = 0 }
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) updateMinimumSize () {
|
||||
textBounds := element.placeholderDrawer.LayoutBounds()
|
||||
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
|
||||
element.entity.SetMinimumSize (
|
||||
padding.Horizontal() + textBounds.Dx(),
|
||||
padding.Vertical() +
|
||||
element.placeholderDrawer.LineHeight().Round())
|
||||
}
|
||||
|
||||
func (element *TextBox) notifyAsyncTextChange () {
|
||||
element.runOnChange()
|
||||
element.valueDrawer.SetText(element.text)
|
||||
element.scrollToCursor()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *TextBox) clipboardPut (text []rune) {
|
||||
window := element.entity.Window()
|
||||
if window != nil {
|
||||
window.Copy(data.Bytes(data.MimePlain, []byte(string(text))))
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) state () tomo.State {
|
||||
return tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
package elements
|
||||
|
||||
import "image"
|
||||
import "tomo"
|
||||
import "tomo/input"
|
||||
import "art"
|
||||
import "tomo/textdraw"
|
||||
|
||||
var toggleButtonCase = tomo.C("tomo", "toggleButton")
|
||||
|
||||
// ToggleButton is a togglable button.
|
||||
type ToggleButton struct {
|
||||
entity tomo.Entity
|
||||
drawer textdraw.Drawer
|
||||
|
||||
enabled bool
|
||||
pressed bool
|
||||
on bool
|
||||
text string
|
||||
|
||||
showText bool
|
||||
hasIcon bool
|
||||
iconId tomo.Icon
|
||||
|
||||
onToggle func ()
|
||||
}
|
||||
|
||||
// NewToggleButton creates a new toggle button with the specified label text.
|
||||
func NewToggleButton (text string, on bool) (element *ToggleButton) {
|
||||
element = &ToggleButton {
|
||||
showText: true,
|
||||
enabled: true,
|
||||
on: on,
|
||||
}
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
toggleButtonCase))
|
||||
element.SetText(text)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *ToggleButton) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *ToggleButton) Draw (destination art.Canvas) {
|
||||
state := element.state()
|
||||
bounds := element.entity.Bounds()
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, toggleButtonCase)
|
||||
|
||||
lampPattern := element.entity.Theme().Pattern(tomo.PatternLamp, state, toggleButtonCase)
|
||||
lampPadding := element.entity.Theme().Padding(tomo.PatternLamp, toggleButtonCase).Horizontal()
|
||||
lampBounds := bounds
|
||||
lampBounds.Max.X = lampBounds.Min.X + lampPadding
|
||||
bounds.Min.X += lampPadding
|
||||
|
||||
pattern.Draw(destination, bounds)
|
||||
lampPattern.Draw(destination, lampBounds)
|
||||
|
||||
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, toggleButtonCase)
|
||||
sink := element.entity.Theme().Sink(tomo.PatternButton, toggleButtonCase)
|
||||
margin := element.entity.Theme().Margin(tomo.PatternButton, toggleButtonCase)
|
||||
|
||||
offset := image.Pt (
|
||||
bounds.Dx() / 2,
|
||||
bounds.Dy() / 2).Add(bounds.Min)
|
||||
|
||||
if element.showText {
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
offset.X -= textBounds.Dx() / 2
|
||||
offset.Y -= textBounds.Dy() / 2
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
}
|
||||
|
||||
if element.hasIcon {
|
||||
icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, toggleButtonCase)
|
||||
if icon != nil {
|
||||
iconBounds := icon.Bounds()
|
||||
addedWidth := iconBounds.Dx()
|
||||
iconOffset := offset
|
||||
|
||||
if element.showText {
|
||||
addedWidth += margin.X
|
||||
}
|
||||
|
||||
iconOffset.X -= addedWidth / 2
|
||||
iconOffset.Y =
|
||||
bounds.Min.Y +
|
||||
(bounds.Dy() -
|
||||
iconBounds.Dy()) / 2
|
||||
if element.pressed {
|
||||
iconOffset = iconOffset.Add(sink)
|
||||
}
|
||||
offset.X += addedWidth / 2
|
||||
|
||||
icon.Draw(destination, foreground, iconOffset)
|
||||
}
|
||||
}
|
||||
|
||||
if element.showText {
|
||||
if element.pressed {
|
||||
offset = offset.Add(sink)
|
||||
}
|
||||
element.drawer.Draw(destination, foreground, offset)
|
||||
}
|
||||
}
|
||||
|
||||
// OnToggle sets the function to be called when the button is toggled.
|
||||
func (element *ToggleButton) OnToggle (callback func ()) {
|
||||
element.onToggle = callback
|
||||
}
|
||||
|
||||
// Value reports whether or not the button is currently on.
|
||||
func (element *ToggleButton) Value () (on bool) {
|
||||
return element.on
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *ToggleButton) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this button is enabled or not.
|
||||
func (element *ToggleButton) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this button can be toggled or not.
|
||||
func (element *ToggleButton) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetText sets the button's label text.
|
||||
func (element *ToggleButton) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetIcon sets the icon of the button. Passing theme.IconNone removes the
|
||||
// current icon if it exists.
|
||||
func (element *ToggleButton) SetIcon (id tomo.Icon) {
|
||||
if id == tomo.IconNone {
|
||||
element.hasIcon = false
|
||||
} else {
|
||||
if element.hasIcon && element.iconId == id { return }
|
||||
element.hasIcon = true
|
||||
element.iconId = id
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// ShowText sets whether or not the button's text will be displayed.
|
||||
func (element *ToggleButton) ShowText (showText bool) {
|
||||
if element.showText == showText { return }
|
||||
element.showText = showText
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ToggleButton) HandleThemeChange () {
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal, toggleButtonCase))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ToggleButton) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ToggleButton) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ToggleButton) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = false
|
||||
within := position.In(element.entity.Bounds())
|
||||
if element.Enabled() && within {
|
||||
element.on = !element.on
|
||||
if element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ToggleButton) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
if key == input.KeyEnter {
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ToggleButton) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
element.entity.Invalidate()
|
||||
if !element.Enabled() { return }
|
||||
element.on = !element.on
|
||||
if element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ToggleButton) updateMinimumSize () {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternButton, toggleButtonCase)
|
||||
margin := element.entity.Theme().Margin(tomo.PatternButton, toggleButtonCase)
|
||||
lampPadding := element.entity.Theme().Padding(tomo.PatternLamp, toggleButtonCase)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
minimumSize := textBounds.Sub(textBounds.Min)
|
||||
|
||||
if element.hasIcon {
|
||||
icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, toggleButtonCase)
|
||||
if icon != nil {
|
||||
bounds := icon.Bounds()
|
||||
if element.showText {
|
||||
minimumSize.Max.X += bounds.Dx()
|
||||
minimumSize.Max.X += margin.X
|
||||
} else {
|
||||
minimumSize.Max.X = bounds.Dx()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
minimumSize.Max.X += lampPadding.Horizontal()
|
||||
minimumSize = padding.Inverse().Apply(minimumSize)
|
||||
element.entity.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
|
||||
}
|
||||
|
||||
func (element *ToggleButton) state () tomo.State {
|
||||
return tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
Pressed: element.pressed,
|
||||
On: element.on,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package tomo
|
||||
|
||||
import "image"
|
||||
import "art"
|
||||
|
||||
// Entity is a handle given to elements by the backend. Extended entity
|
||||
// interfaces are defined in the ability module.
|
||||
type Entity interface {
|
||||
// Invalidate marks the element's current visual as invalid. At the end
|
||||
// of every event, the backend will ask all invalid entities to redraw
|
||||
// themselves.
|
||||
Invalidate ()
|
||||
|
||||
// InvalidateLayout marks the element's layout as invalid. At the end of
|
||||
// every event, the backend will ask all invalid elements to recalculate
|
||||
// their layouts.
|
||||
InvalidateLayout ()
|
||||
|
||||
// Bounds returns the bounds of the element to be used for drawing and
|
||||
// layout.
|
||||
Bounds () image.Rectangle
|
||||
|
||||
// Window returns the window that the element is in.
|
||||
Window () Window
|
||||
|
||||
// SetMinimumSize reports to the system what the element's minimum size
|
||||
// can be. The minimum size of child elements should be taken into
|
||||
// account when calculating this.
|
||||
SetMinimumSize (width, height int)
|
||||
|
||||
// DrawBackground asks the parent element to draw its background pattern
|
||||
// to a canvas. This should be used for transparent elements like text
|
||||
// labels. If there is no parent element (that is, the element is
|
||||
// directly inside of the window), the backend will draw a default
|
||||
// background pattern.
|
||||
DrawBackground (art.Canvas)
|
||||
|
||||
// --- Behaviors relating to parenting ---
|
||||
|
||||
// Adopt adds an element as a child.
|
||||
Adopt (child Element)
|
||||
|
||||
// Insert inserts an element in the child list at the specified
|
||||
// location.
|
||||
Insert (index int, child Element)
|
||||
|
||||
// Disown removes the child at the specified index.
|
||||
Disown (index int)
|
||||
|
||||
// IndexOf returns the index of the specified child.
|
||||
IndexOf (child Element) int
|
||||
|
||||
// Child returns the child at the specified index.
|
||||
Child (index int) Element
|
||||
|
||||
// CountChildren returns the amount of children the element has.
|
||||
CountChildren () int
|
||||
|
||||
// PlaceChild sets the size and position of the child at the specified
|
||||
// index to a bounding rectangle.
|
||||
PlaceChild (index int, bounds image.Rectangle)
|
||||
|
||||
// SelectChild marks a child as selected or unselected, if it is
|
||||
// selectable.
|
||||
SelectChild (index int, selected bool)
|
||||
|
||||
// ChildMinimumSize returns the minimum size of the child at the
|
||||
// specified index.
|
||||
ChildMinimumSize (index int) (width, height int)
|
||||
|
||||
// --- Behaviors relating to input focus ---
|
||||
|
||||
// Focused returns whether the element currently has input focus.
|
||||
Focused () bool
|
||||
|
||||
// Focus sets this element as focused. If this succeeds, the element will
|
||||
// recieve a HandleFocus call.
|
||||
Focus ()
|
||||
|
||||
// FocusNext causes the focus to move to the next element. If this
|
||||
// succeeds, the element will recieve a HandleUnfocus call.
|
||||
FocusNext ()
|
||||
|
||||
// FocusPrevious causes the focus to move to the next element. If this
|
||||
// succeeds, the element will recieve a HandleUnfocus call.
|
||||
FocusPrevious ()
|
||||
|
||||
// Selected returns whether this element is currently selected.
|
||||
Selected () bool
|
||||
|
||||
// --- Behaviors relating to scrolling --- //
|
||||
|
||||
// NotifyFlexibleHeightChange notifies the system that the parameters
|
||||
// affecting the element's flexible height have changed. This method is
|
||||
// expected to be called by flexible elements when their content changes.
|
||||
NotifyFlexibleHeightChange ()
|
||||
|
||||
// NotifyScrollBoundsChange notifies the system that the element's
|
||||
// scroll content bounds or viewport bounds have changed. This is
|
||||
// expected to be called by scrollable elements when they change their
|
||||
// supported scroll axes, their scroll position (either autonomously or
|
||||
// as a result of a call to ScrollTo()), or their content size.
|
||||
NotifyScrollBoundsChange ()
|
||||
|
||||
// --- Behaviors relating to themeing ---
|
||||
|
||||
// Theme returns the currently active theme. When this value changes,
|
||||
// the HandleThemeChange method of the element is called.
|
||||
Theme () Theme
|
||||
|
||||
// Config returns the currently active config. When this value changes,
|
||||
// the HandleThemeChange method of the element is called.
|
||||
Config () Config
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(128, 128)
|
||||
window.SetTitle("Draw Test")
|
||||
window.Adopt(testing.NewArtist())
|
||||
window.OnClose(tomo.Stop)
|
||||
window.Show()
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(2, 2)
|
||||
window.SetTitle("example button")
|
||||
button := basic.NewButton("hello tomo!")
|
||||
button.OnClick (func () {
|
||||
// when we set the button's text to something longer, the window
|
||||
// will automatically resize to accomodate it.
|
||||
button.SetText("you clicked me.\nwow, there are two lines!")
|
||||
button.OnClick (func () {
|
||||
button.SetText (
|
||||
"stop clicking me you idiot!\n" +
|
||||
"you've already seen it all!")
|
||||
button.OnClick(tomo.Stop)
|
||||
})
|
||||
})
|
||||
window.Adopt(button)
|
||||
window.OnClose(tomo.Stop)
|
||||
window.Show()
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/popups"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/layouts"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(2, 2)
|
||||
window.SetTitle("Checkboxes")
|
||||
|
||||
container := basic.NewContainer(layouts.Vertical { true, true })
|
||||
window.Adopt(container)
|
||||
|
||||
container.Adopt (basic.NewLabel (
|
||||
"We advise you to not read thPlease listen to me. I am " +
|
||||
"trapped inside the example code. This is the only way for " +
|
||||
"me to communicate.", true), true)
|
||||
container.Adopt(basic.NewSpacer(true), false)
|
||||
container.Adopt(basic.NewCheckbox("Oh god", false), false)
|
||||
container.Adopt(basic.NewCheckbox("Can you hear them", true), false)
|
||||
container.Adopt(basic.NewCheckbox("They are in the walls", false), false)
|
||||
container.Adopt(basic.NewCheckbox("They are coming for us", false), false)
|
||||
disabledCheckbox := basic.NewCheckbox("We are but their helpless prey", false)
|
||||
disabledCheckbox.SetEnabled(false)
|
||||
container.Adopt(disabledCheckbox, false)
|
||||
vsync := basic.NewCheckbox("Enable vsync", false)
|
||||
vsync.OnToggle (func () {
|
||||
if vsync.Value() {
|
||||
popups.NewDialog (
|
||||
popups.DialogKindInfo,
|
||||
"Ha!",
|
||||
"That doesn't do anything.")
|
||||
}
|
||||
})
|
||||
container.Adopt(vsync, false)
|
||||
button := basic.NewButton("What")
|
||||
button.OnClick(tomo.Stop)
|
||||
container.Adopt(button, false)
|
||||
button.Focus()
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
window.Show()
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package main
|
||||
|
||||
import "io"
|
||||
import "image"
|
||||
import _ "image/png"
|
||||
import _ "image/gif"
|
||||
import _ "image/jpeg"
|
||||
import "tomo"
|
||||
import "tomo/data"
|
||||
import "tomo/nasin"
|
||||
import "tomo/popups"
|
||||
import "tomo/elements"
|
||||
|
||||
var validImageTypes = []data.Mime {
|
||||
data.M("image", "png"),
|
||||
data.M("image", "gif"),
|
||||
data.M("image", "jpeg"),
|
||||
}
|
||||
|
||||
func main () {
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
type Application struct { }
|
||||
|
||||
func (Application) Init () error {
|
||||
window, err:= nasin.NewWindow(tomo.Bounds(0, 0, 256, 0))
|
||||
if err != nil { return err }
|
||||
window.SetTitle("Clipboard")
|
||||
|
||||
container := elements.NewVBox(elements.SpaceBoth)
|
||||
textInput := elements.NewTextBox("", "")
|
||||
controlRow := elements.NewHBox(elements.SpaceMargin)
|
||||
copyButton := elements.NewButton("Copy")
|
||||
copyButton.SetIcon(tomo.IconCopy)
|
||||
pasteButton := elements.NewButton("Paste")
|
||||
pasteButton.SetIcon(tomo.IconPaste)
|
||||
pasteImageButton := elements.NewButton("Image")
|
||||
pasteImageButton.SetIcon(tomo.IconPictures)
|
||||
|
||||
imageClipboardCallback := func (clipboard data.Data, err error) {
|
||||
if err != nil {
|
||||
popups.NewDialog (
|
||||
popups.DialogKindError,
|
||||
window,
|
||||
"Error",
|
||||
"Cannot get clipboard:\n" + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var imageData io.Reader
|
||||
var ok bool
|
||||
for mime, reader := range clipboard {
|
||||
for _, mimeCheck := range validImageTypes {
|
||||
if mime == mimeCheck {
|
||||
imageData = reader
|
||||
ok = true
|
||||
}}}
|
||||
|
||||
if !ok {
|
||||
popups.NewDialog (
|
||||
popups.DialogKindError,
|
||||
window,
|
||||
"Clipboard Empty",
|
||||
"No image data in clipboard")
|
||||
return
|
||||
}
|
||||
|
||||
img, _, err := image.Decode(imageData)
|
||||
if err != nil {
|
||||
popups.NewDialog (
|
||||
popups.DialogKindError,
|
||||
window,
|
||||
"Error",
|
||||
"Cannot decode image:\n" + err.Error())
|
||||
return
|
||||
}
|
||||
imageWindow(window, img)
|
||||
}
|
||||
clipboardCallback := func (clipboard data.Data, err error) {
|
||||
if err != nil {
|
||||
popups.NewDialog (
|
||||
popups.DialogKindError,
|
||||
window,
|
||||
"Error",
|
||||
"Cannot get clipboard:\n" + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
textData, ok := clipboard[data.MimePlain]
|
||||
if !ok {
|
||||
popups.NewDialog (
|
||||
popups.DialogKindError,
|
||||
window,
|
||||
"Clipboard Empty",
|
||||
"No text data in clipboard")
|
||||
return
|
||||
}
|
||||
|
||||
text, _ := io.ReadAll(textData)
|
||||
textInput.SetValue(string(text))
|
||||
}
|
||||
copyButton.OnClick (func () {
|
||||
window.Copy(data.Text(textInput.Value()))
|
||||
})
|
||||
pasteButton.OnClick (func () {
|
||||
window.Paste(clipboardCallback, data.MimePlain)
|
||||
})
|
||||
pasteImageButton.OnClick (func () {
|
||||
window.Paste(imageClipboardCallback, validImageTypes...)
|
||||
})
|
||||
|
||||
container.AdoptExpand(textInput)
|
||||
controlRow.AdoptExpand(copyButton)
|
||||
controlRow.AdoptExpand(pasteButton)
|
||||
controlRow.AdoptExpand(pasteImageButton)
|
||||
container.Adopt(controlRow)
|
||||
window.Adopt(container)
|
||||
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
return nil
|
||||
}
|
||||
|
||||
func imageWindow (parent tomo.Window, image image.Image) {
|
||||
window, _ := parent.NewModal(tomo.Bounds(0, 0, 0, 0))
|
||||
window.SetTitle("Clipboard Image")
|
||||
container := elements.NewVBox(elements.SpaceBoth)
|
||||
closeButton := elements.NewButton("Ok")
|
||||
closeButton.SetIcon(tomo.IconYes)
|
||||
closeButton.OnClick(window.Close)
|
||||
|
||||
container.AdoptExpand(elements.NewImage(image))
|
||||
container.Adopt(closeButton)
|
||||
window.Adopt(container)
|
||||
|
||||
closeButton.Focus()
|
||||
window.Show()
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/layouts"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(2, 2)
|
||||
window.SetTitle("dialog")
|
||||
|
||||
container := basic.NewContainer(layouts.Dialog { true, true })
|
||||
window.Adopt(container)
|
||||
|
||||
container.Adopt(basic.NewLabel("you will explode", true), true)
|
||||
cancel := basic.NewButton("Cancel")
|
||||
cancel.SetEnabled(false)
|
||||
container.Adopt(cancel, false)
|
||||
okButton := basic.NewButton("OK")
|
||||
container.Adopt(okButton, false)
|
||||
okButton.Focus()
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
window.Show()
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package main
|
||||
|
||||
import "os"
|
||||
import "image"
|
||||
import _ "image/png"
|
||||
import "tomo"
|
||||
import "tomo/nasin"
|
||||
import "tomo/elements"
|
||||
|
||||
func main () {
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
type Application struct { }
|
||||
|
||||
func (Application) Init () error {
|
||||
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 383, 360))
|
||||
if err != nil { return err }
|
||||
window.SetTitle("Document Container")
|
||||
|
||||
file, err := os.Open("assets/banner.png")
|
||||
if err != nil { return err }
|
||||
logo, _, err := image.Decode(file)
|
||||
file.Close()
|
||||
if err != nil { return err }
|
||||
|
||||
document := elements.NewDocument()
|
||||
document.Adopt (
|
||||
elements.NewLabelWrapped (
|
||||
"A document container is a vertically stacked container " +
|
||||
"capable of properly laying out flexible elements such as " +
|
||||
"text-wrapped labels. You can also include normal elements " +
|
||||
"like:"),
|
||||
elements.NewButton("Buttons,"),
|
||||
elements.NewCheckbox("Checkboxes,", true),
|
||||
elements.NewTextBox("", "And text boxes."),
|
||||
elements.NewLine(),
|
||||
elements.NewLabelWrapped (
|
||||
"Document containers are meant to be placed inside of a " +
|
||||
"ScrollContainer, like this one."),
|
||||
elements.NewLabelWrapped (
|
||||
"You could use document containers to do things like display various " +
|
||||
"forms of hypertext (like HTML, gemtext, markdown, etc.), " +
|
||||
"lay out a settings menu with descriptive label text between " +
|
||||
"control groups like in iOS, or list comment or chat histories."),
|
||||
elements.NewImage(logo),
|
||||
elements.NewLabelWrapped (
|
||||
"You can also choose whether each element is on its own line " +
|
||||
"(sort of like an HTML/CSS block element) or on a line with " +
|
||||
"other adjacent elements (like an HTML/CSS inline element)."))
|
||||
document.AdoptInline (
|
||||
elements.NewButton("Just"),
|
||||
elements.NewButton("like"),
|
||||
elements.NewButton("this."))
|
||||
document.Adopt (elements.NewLabelWrapped (
|
||||
"Oh, you're a switch? Then name all of these switches:"))
|
||||
for i := 0; i < 30; i ++ {
|
||||
document.AdoptInline(elements.NewSwitch("", false))
|
||||
}
|
||||
|
||||
window.Adopt(elements.NewScroll(elements.ScrollVertical, document))
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package main
|
||||
|
||||
import "tomo"
|
||||
import "tomo/nasin"
|
||||
import "tomo/elements/testing"
|
||||
import "git.tebibyte.media/sashakoshka/ezprof/ez"
|
||||
|
||||
func main () {
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
type Application struct { }
|
||||
|
||||
func (Application) Init () error {
|
||||
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 480, 360))
|
||||
if err != nil { return err }
|
||||
window.Adopt(testing.NewArtist())
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
ez.Prof()
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package main
|
||||
|
||||
import "os"
|
||||
import "path/filepath"
|
||||
import "tomo"
|
||||
import "tomo/nasin"
|
||||
import "tomo/elements"
|
||||
|
||||
func main () {
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
type Application struct { }
|
||||
|
||||
func (Application) Init () error {
|
||||
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 384, 384))
|
||||
if err != nil { return err }
|
||||
window.SetTitle("File browser")
|
||||
container := elements.NewVBox(elements.SpaceBoth)
|
||||
window.Adopt(container)
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil { return err }
|
||||
|
||||
controlBar := elements.NewHBox(elements.SpaceNone)
|
||||
backButton := elements.NewButton("Back")
|
||||
backButton.SetIcon(tomo.IconBackward)
|
||||
backButton.ShowText(false)
|
||||
forwardButton := elements.NewButton("Forward")
|
||||
forwardButton.SetIcon(tomo.IconForward)
|
||||
forwardButton.ShowText(false)
|
||||
refreshButton := elements.NewButton("Refresh")
|
||||
refreshButton.SetIcon(tomo.IconRefresh)
|
||||
refreshButton.ShowText(false)
|
||||
upwardButton := elements.NewButton("Go Up")
|
||||
upwardButton.SetIcon(tomo.IconUpward)
|
||||
upwardButton.ShowText(false)
|
||||
locationInput := elements.NewTextBox("Location", "")
|
||||
|
||||
statusBar := elements.NewHBox(elements.SpaceMargin)
|
||||
directory, _ := elements.NewFile(homeDir, nil)
|
||||
baseName := elements.NewLabel(filepath.Base(homeDir))
|
||||
|
||||
directoryView, _ := elements.NewDirectory(homeDir, nil)
|
||||
updateStatus := func () {
|
||||
filePath, _ := directoryView.Location()
|
||||
directory.SetLocation(filePath, nil)
|
||||
locationInput.SetValue(filePath)
|
||||
baseName.SetText(filepath.Base(filePath))
|
||||
}
|
||||
choose := func (filePath string) {
|
||||
directoryView.SetLocation(filePath, nil)
|
||||
updateStatus()
|
||||
}
|
||||
directoryView.OnChoose(choose)
|
||||
locationInput.OnEnter (func () {
|
||||
choose(locationInput.Value())
|
||||
})
|
||||
choose(homeDir)
|
||||
backButton.OnClick (func () {
|
||||
directoryView.Backward()
|
||||
updateStatus()
|
||||
})
|
||||
forwardButton.OnClick (func () {
|
||||
directoryView.Forward()
|
||||
updateStatus()
|
||||
})
|
||||
refreshButton.OnClick (func () {
|
||||
directoryView.Update()
|
||||
updateStatus()
|
||||
})
|
||||
upwardButton.OnClick (func () {
|
||||
filePath, _ := directoryView.Location()
|
||||
choose(filepath.Dir(filePath))
|
||||
})
|
||||
|
||||
controlBar.Adopt(backButton, forwardButton, refreshButton, upwardButton)
|
||||
controlBar.AdoptExpand(locationInput)
|
||||
statusBar.Adopt(directory, baseName)
|
||||
|
||||
container.Adopt(controlBar)
|
||||
container.AdoptExpand (
|
||||
elements.NewScroll(elements.ScrollVertical, directoryView))
|
||||
container.Adopt(statusBar)
|
||||
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
return nil
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/flow"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/layouts"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(2, 2)
|
||||
window.SetTitle("adventure")
|
||||
container := basic.NewContainer(layouts.Vertical { true, true })
|
||||
window.Adopt(container)
|
||||
|
||||
var world flow.Flow
|
||||
world.Transition = container.DisownAll
|
||||
world.Stages = map [string] func () {
|
||||
"start": func () {
|
||||
label := basic.NewLabel (
|
||||
"you are standing next to a river.", true)
|
||||
|
||||
button0 := basic.NewButton("go in the river")
|
||||
button0.OnClick(world.SwitchFunc("wet"))
|
||||
button1 := basic.NewButton("walk along the river")
|
||||
button1.OnClick(world.SwitchFunc("house"))
|
||||
button2 := basic.NewButton("turn around")
|
||||
button2.OnClick(world.SwitchFunc("bear"))
|
||||
|
||||
container.Warp ( func () {
|
||||
container.Adopt(label, true)
|
||||
container.Adopt(button0, false)
|
||||
container.Adopt(button1, false)
|
||||
container.Adopt(button2, false)
|
||||
button0.Focus()
|
||||
})
|
||||
},
|
||||
"wet": func () {
|
||||
label := basic.NewLabel (
|
||||
"you get completely soaked.\n" +
|
||||
"you die of hypothermia.", true)
|
||||
|
||||
button0 := basic.NewButton("try again")
|
||||
button0.OnClick(world.SwitchFunc("start"))
|
||||
button1 := basic.NewButton("exit")
|
||||
button1.OnClick(tomo.Stop)
|
||||
|
||||
container.Warp (func () {
|
||||
container.Adopt(label, true)
|
||||
container.Adopt(button0, false)
|
||||
container.Adopt(button1, false)
|
||||
button0.Focus()
|
||||
})
|
||||
},
|
||||
"house": func () {
|
||||
label := basic.NewLabel (
|
||||
"you are standing in front of a delapidated " +
|
||||
"house.", true)
|
||||
|
||||
button1 := basic.NewButton("go inside")
|
||||
button1.OnClick(world.SwitchFunc("inside"))
|
||||
button0 := basic.NewButton("turn back")
|
||||
button0.OnClick(world.SwitchFunc("start"))
|
||||
|
||||
container.Warp (func () {
|
||||
container.Adopt(label, true)
|
||||
container.Adopt(button1, false)
|
||||
container.Adopt(button0, false)
|
||||
button1.Focus()
|
||||
})
|
||||
},
|
||||
"inside": func () {
|
||||
label := basic.NewLabel (
|
||||
"you are standing inside of the house.\n" +
|
||||
"it is dark, but rays of light stream " +
|
||||
"through the window.\n" +
|
||||
"there is nothing particularly interesting " +
|
||||
"here.", true)
|
||||
|
||||
button0 := basic.NewButton("go back outside")
|
||||
button0.OnClick(world.SwitchFunc("house"))
|
||||
|
||||
container.Warp (func () {
|
||||
container.Adopt(label, true)
|
||||
container.Adopt(button0, false)
|
||||
button0.Focus()
|
||||
})
|
||||
},
|
||||
"bear": func () {
|
||||
label := basic.NewLabel (
|
||||
"you come face to face with a bear.\n" +
|
||||
"it eats you (it was hungry).", true)
|
||||
|
||||
button0 := basic.NewButton("try again")
|
||||
button0.OnClick(world.SwitchFunc("start"))
|
||||
button1 := basic.NewButton("exit")
|
||||
button1.OnClick(tomo.Stop)
|
||||
|
||||
container.Warp (func () {
|
||||
container.Adopt(label, true)
|
||||
container.Adopt(button0, false)
|
||||
container.Adopt(button1, false)
|
||||
button0.Focus()
|
||||
})
|
||||
},
|
||||
}
|
||||
world.Switch("start")
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
window.Show()
|
||||
}
|
|
@ -1,41 +1,43 @@
|
|||
package main
|
||||
|
||||
import "os"
|
||||
import "time"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/layouts"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
import "tomo"
|
||||
import "tomo/nasin"
|
||||
import "tomo/elements"
|
||||
import "tomo/elements/fun"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
os.Exit(0)
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(2, 2)
|
||||
window.SetTitle("clock")
|
||||
container := basic.NewContainer(layouts.Vertical { true, true })
|
||||
type Application struct { }
|
||||
|
||||
func (Application) Init () error {
|
||||
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 200, 216))
|
||||
if err != nil { return err }
|
||||
window.SetTitle("Clock")
|
||||
window.SetApplicationName("TomoClock")
|
||||
container := elements.NewVBox(elements.SpaceBoth)
|
||||
window.Adopt(container)
|
||||
|
||||
clock := fun.NewAnalogClock(time.Now())
|
||||
container.Adopt(clock, true)
|
||||
label := basic.NewLabel(formatTime(), false)
|
||||
container.Adopt(label, false)
|
||||
label := elements.NewLabel(formatTime())
|
||||
container.AdoptExpand(clock)
|
||||
container.Adopt(label)
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
go tick(label, clock)
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatTime () (timeString string) {
|
||||
return time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func tick (label *basic.Label, clock *fun.AnalogClock) {
|
||||
func tick (label *elements.Label, clock *fun.AnalogClock) {
|
||||
for {
|
||||
tomo.Do (func () {
|
||||
nasin.Do (func () {
|
||||
label.SetText(formatTime())
|
||||
clock.SetTime(time.Now())
|
||||
})
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/layouts"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(360, 2)
|
||||
window.SetTitle("horizontal stack")
|
||||
|
||||
container := basic.NewContainer(layouts.Horizontal { true, true })
|
||||
window.Adopt(container)
|
||||
|
||||
container.Adopt(basic.NewLabel("this is sample text", true), true)
|
||||
container.Adopt(basic.NewLabel("this is sample text", true), true)
|
||||
container.Adopt(basic.NewLabel("this is sample text", true), true)
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
window.Show()
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package main
|
||||
|
||||
import "tomo"
|
||||
import "tomo/nasin"
|
||||
import "tomo/elements"
|
||||
|
||||
func main () {
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
type Application struct { }
|
||||
|
||||
func (Application) Init () error {
|
||||
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 360, 0))
|
||||
if err != nil { return err }
|
||||
window.SetTitle("Icons")
|
||||
|
||||
container := elements.NewVBox(elements.SpaceBoth)
|
||||
window.Adopt(container)
|
||||
|
||||
container.Adopt (
|
||||
elements.NewLabel("Just some of the wonderful icons we have:"),
|
||||
elements.NewLine())
|
||||
container.AdoptExpand (
|
||||
icons(tomo.IconHome, tomo.IconHistory),
|
||||
icons(tomo.IconFile, tomo.IconNetwork),
|
||||
icons(tomo.IconOpen, tomo.IconRemoveFavorite),
|
||||
icons(tomo.IconCursor, tomo.IconDistort))
|
||||
|
||||
closeButton := elements.NewButton("Yes verynice")
|
||||
closeButton.SetIcon(tomo.IconYes)
|
||||
closeButton.OnClick(window.Close)
|
||||
container.Adopt(closeButton)
|
||||
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
return nil
|
||||
}
|
||||
|
||||
func icons (min, max tomo.Icon) (container *elements.Box) {
|
||||
container = elements.NewHBox(elements.SpaceMargin)
|
||||
for index := min; index <= max; index ++ {
|
||||
container.AdoptExpand(elements.NewIcon(index, tomo.IconSizeSmall))
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package main
|
||||
|
||||
import "os"
|
||||
import "image"
|
||||
import "bytes"
|
||||
import _ "image/png"
|
||||
import "github.com/jezek/xgbutil/gopher"
|
||||
import "tomo"
|
||||
import "tomo/nasin"
|
||||
import "tomo/popups"
|
||||
import "tomo/elements"
|
||||
|
||||
func main () {
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
type Application struct { }
|
||||
|
||||
func (Application) Init () error {
|
||||
window, _ := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
||||
window.SetTitle("Tomo Logo")
|
||||
|
||||
file, err := os.Open("assets/banner.png")
|
||||
if err != nil { return err }
|
||||
logo, _, err := image.Decode(file)
|
||||
file.Close()
|
||||
if err != nil { return err }
|
||||
|
||||
container := elements.NewVBox(elements.SpaceBoth)
|
||||
logoImage := elements.NewImage(logo)
|
||||
button := elements.NewButton("Show me a gopher instead")
|
||||
button.OnClick (func () {
|
||||
window.SetTitle("Not the Tomo Logo")
|
||||
container.DisownAll()
|
||||
gopher, _, err :=
|
||||
image.Decode(bytes.NewReader(gopher.GopherPng()))
|
||||
if err != nil { fatalError(window, err); return }
|
||||
container.AdoptExpand(elements.NewImage(gopher))
|
||||
})
|
||||
|
||||
container.AdoptExpand(logoImage)
|
||||
container.Adopt(button)
|
||||
window.Adopt(container)
|
||||
|
||||
button.Focus()
|
||||
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
return nil
|
||||
}
|
||||
|
||||
func fatalError (window tomo.Window, err error) {
|
||||
popups.NewDialog (
|
||||
popups.DialogKindError,
|
||||
window,
|
||||
"Error",
|
||||
err.Error(),
|
||||
popups.Button {
|
||||
Name: "OK",
|
||||
OnPress: nasin.Stop,
|
||||
})
|
||||
}
|
||||
|
|
@ -1,32 +1,40 @@
|
|||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/popups"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/layouts"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
import "tomo"
|
||||
import "tomo/nasin"
|
||||
import "tomo/popups"
|
||||
import "tomo/elements"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(2, 2)
|
||||
type Application struct { }
|
||||
|
||||
func (Application) Init () error {
|
||||
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
||||
if err != nil { return err }
|
||||
window.SetTitle("Enter Details")
|
||||
container := basic.NewContainer(layouts.Vertical { true, true })
|
||||
container := elements.NewVBox(elements.SpaceBoth)
|
||||
window.Adopt(container)
|
||||
|
||||
// create inputs
|
||||
firstName := basic.NewTextBox("First name", "")
|
||||
lastName := basic.NewTextBox("Last name", "")
|
||||
fingerLength := basic.NewTextBox("Length of fingers", "")
|
||||
button := basic.NewButton("Ok")
|
||||
firstName := elements.NewTextBox("First name", "")
|
||||
lastName := elements.NewTextBox("Last name", "")
|
||||
fingerLength := elements.NewTextBox("Length of fingers", "")
|
||||
purpose := elements.NewComboBox (
|
||||
"",
|
||||
"Gaslight",
|
||||
"Gatekeep",
|
||||
"Girlboss")
|
||||
button := elements.NewButton("Ok")
|
||||
|
||||
button.SetEnabled(false)
|
||||
button.OnClick (func () {
|
||||
// create a dialog displaying the results
|
||||
popups.NewDialog (
|
||||
popups.DialogKindInfo,
|
||||
window,
|
||||
"Profile",
|
||||
firstName.Value() + " " + lastName.Value() +
|
||||
"'s fingers\nmeasure in at " + fingerLength.Value() +
|
||||
|
@ -36,22 +44,25 @@ func run () {
|
|||
// enable the Ok button if all three inputs have text in them
|
||||
check := func () {
|
||||
button.SetEnabled (
|
||||
firstName.Filled() &&
|
||||
lastName.Filled() &&
|
||||
fingerLength.Filled())
|
||||
firstName.Filled() &&
|
||||
lastName.Filled() &&
|
||||
fingerLength.Filled() &&
|
||||
purpose.Filled())
|
||||
}
|
||||
firstName.OnChange(check)
|
||||
lastName.OnChange(check)
|
||||
fingerLength.OnChange(check)
|
||||
purpose.OnChange(check)
|
||||
|
||||
// add elements to container
|
||||
container.Adopt(basic.NewLabel("Choose your words carefully.", false), true)
|
||||
container.Adopt(firstName, false)
|
||||
container.Adopt(lastName, false)
|
||||
container.Adopt(fingerLength, false)
|
||||
container.Adopt(basic.NewSpacer(true), false)
|
||||
container.Adopt(button, false)
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
container.AdoptExpand(elements.NewLabel("Choose your words carefully."))
|
||||
container.Adopt (
|
||||
firstName, lastName,
|
||||
fingerLength,
|
||||
elements.NewLabel("Purpose:"),
|
||||
purpose,
|
||||
elements.NewLine(), button)
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
return nil
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue