Compare commits

...

278 commits

Author SHA1 Message Date
griffi-gh 55d5146826 k it kinda wokrs 2024-05-08 03:21:49 +02:00
griffi-gh 485f43eefa initial wgpu backend impl 2024-05-08 02:57:03 +02:00
griffi-gh 1c0bb77788 wip wgpu be 2024-05-08 01:50:14 +02:00
griffi-gh ab6f4b5d31 upd readme to mention wgpu 2024-05-08 01:50:02 +02:00
griffi-gh ea9e6d303d remove unused thingy 2024-05-08 01:49:48 +02:00
griffi-gh 2d85b0c422 create hui-wgpu 2024-05-05 01:14:06 +02:00
griffi-gh 6b715149d6 add support for winit 0.30 2024-05-04 21:43:03 +02:00
griffi-gh 2901ca5b82 skip rendering transparent rects 2024-05-04 17:23:39 +02:00
griffi-gh b03e23c439 rename stack_bottom to stack_below 2024-04-22 01:14:43 +02:00
griffi-gh 44824a86f5 for rgb stuff, always return true instead of delegating 2024-04-17 16:28:57 +02:00
griffi-gh fa672f422f use rect om frame api 2024-04-17 16:22:51 +02:00
griffi-gh d62207fbd3 uwu 2024-04-17 16:04:34 +02:00
griffi-gh 4b34137375 a 2024-04-17 16:02:29 +02:00
griffi-gh fda54e01b3 refactor rect_frame macro 2024-04-17 16:00:27 +02:00
griffi-gh f820f7e930 rename FrameRect to RectFrame 2024-04-17 15:57:46 +02:00
griffi-gh bbe5b273d1 lol i forgor to rename the module itself 2024-04-17 15:47:22 +02:00
griffi-gh e7da4a7f0d rename FillRect to FrameView 2024-04-17 15:44:47 +02:00
griffi-gh 0625b91735 uhh stuff 2024-04-17 15:41:10 +02:00
griffi-gh 85ebf67431 allow rect in add, clean up elem api 2024-04-17 15:21:17 +02:00
griffi-gh 7cfc97b434 add state scope function 2024-03-29 14:27:57 +01:00
griffi-gh 512042ebd8 wip state stuff 2024-03-29 13:44:56 +01:00
griffi-gh ef8032c29d Update devcontainer.json 2024-03-26 00:40:54 +01:00
griffi-gh 632ed35d82 Update devcontainer.json 2024-03-26 00:36:04 +01:00
griffi-gh bd2f49d7f2 Create devcontainer.json 2024-03-26 00:25:28 +01:00
griffi-gh 101b516ed1 bump master version to 0.1.0-alpha.5 2024-03-25 18:41:44 +01:00
griffi-gh 4550cefb0b Prepare for publish 2024-03-25 18:35:54 +01:00
griffi-gh 54b1c44159 add note about MSRV 2024-03-25 18:32:31 +01:00
griffi-gh 11b5363e26 add doc_auto_cfg 2024-03-25 18:29:44 +01:00
griffi-gh 3dabfdb339 re-export Signal derive from the signal module 2024-03-25 18:14:36 +01:00
griffi-gh e9078ade10 document some stuff 2024-03-25 18:13:10 +01:00
griffi-gh 9e61c08706 fix texture uv mess 2024-03-25 18:00:15 +01:00
griffi-gh 3f29000136 make last rect shorter in example 7 2024-03-25 17:52:24 +01:00
griffi-gh f4ca1ffbd6 use derive macro in examples 2024-03-25 17:51:34 +01:00
griffi-gh 5755613e90 add derive macro for Signal 2024-03-25 17:49:32 +01:00
griffi-gh 9f3d2c7def use frames for progressbar 2024-03-25 16:16:56 +01:00
griffi-gh b87a0b2f04 load shader based on api 2024-03-25 15:37:32 +01:00
griffi-gh e765eb2748 disable word wrap 2024-03-25 14:26:30 +01:00
griffi-gh a0a24c80dd update readme 2024-03-25 14:26:20 +01:00
griffi-gh c104f6cc04 this should fix the gap thing? 2024-03-25 14:18:14 +01:00
griffi-gh f52a15009b fix gap/padding layout (kinda) for remaining size 2024-03-25 14:14:12 +01:00
griffi-gh 4bb59b7c5f wip Remaing size 2024-03-25 14:08:04 +01:00
griffi-gh adc5cb5f3b more granular control over wrapping 2024-03-25 12:57:45 +01:00
griffi-gh f6ff5e7269 rearrange stuff 2024-03-25 12:40:06 +01:00
griffi-gh 0b5df70e2b add todo note :3 2024-03-25 02:46:30 +01:00
griffi-gh dfcd4af0aa remove fixme comment, fixed by ogl backend change 2024-03-25 02:45:43 +01:00
griffi-gh ccd8c060c7 upd slider 2024-03-25 02:31:57 +01:00
griffi-gh a9e82fccb5 clean up example, make slider work 2024-03-25 02:30:51 +01:00
griffi-gh d9b4e20d70 use linear minify filter 2024-03-25 02:25:46 +01:00
griffi-gh eaadf43821 add fixme note for tomorrow 2024-03-25 02:13:50 +01:00
griffi-gh d8f38fd5b4 add note about slider 2024-03-25 02:10:15 +01:00
griffi-gh f6ea37e402 aaa 2024-03-25 02:06:23 +01:00
griffi-gh bfb5bfaa0a add custom slider as an example 2024-03-25 02:05:26 +01:00
griffi-gh 1666b08873 add 9 patch rendering 2024-03-25 01:59:13 +01:00
griffi-gh d11f160d55 disallow opts for rounded rects 2024-03-24 23:59:27 +01:00
griffi-gh 88a2b56610 aaa 2024-03-24 22:55:36 +01:00
griffi-gh 84fef8a6f8 prepare test 2024-03-24 22:55:15 +01:00
griffi-gh 72277a0fed add image loading 2024-03-24 22:55:05 +01:00
griffi-gh 7ce24185e5 allow customizing handle size 2024-03-24 22:27:59 +01:00
griffi-gh a7f01b1a72 upd api to frame 2024-03-24 22:18:15 +01:00
griffi-gh aee38bec45 disable covers_opaque optimization for images 2024-03-24 22:01:36 +01:00
griffi-gh 5af38d7a2a add covers_opaque 2024-03-24 21:35:44 +01:00
griffi-gh e488ef70b5 slider: use frames (less efficient :<) 2024-03-24 21:14:24 +01:00
griffi-gh e4fc96a2d8 :3 2024-03-24 21:01:30 +01:00
griffi-gh 17a313bdb8 update glam to 0.27 2024-03-24 21:00:46 +01:00
griffi-gh 4e17d368db format the thingy 2024-03-24 20:58:14 +01:00
griffi-gh 0eec840a08 update built-in element features 2024-03-24 20:53:03 +01:00
griffi-gh 84fdd9d1ae uwu 2024-03-24 19:08:24 +01:00
griffi-gh 3f8dea24be improve frame_rect macro docs 2024-03-24 19:07:51 +01:00
griffi-gh 39746aa566 use the new macro in examples 2024-03-24 18:32:50 +01:00
griffi-gh 47c5f1c4a8 add todo 2024-03-24 02:26:56 +01:00
griffi-gh b18e2a5066 update align_test, drop rounded_test 2024-03-24 02:24:39 +01:00
griffi-gh 9b7be50880 make hacky macro even hackier 2024-03-24 02:19:26 +01:00
griffi-gh 04ebfd41e8 frame_rect macro 2024-03-24 02:05:28 +01:00
griffi-gh 27a75d6c30 update most examples to new ugly api 2024-03-24 01:28:03 +01:00
griffi-gh 2e49ea443a fix fn name in ex.6 2024-03-24 01:17:26 +01:00
griffi-gh e665a94335 remove deprecated background fn 2024-03-24 01:16:51 +01:00
griffi-gh 5cb696d3f6 simplify with_background_frame call in example 2024-03-24 01:14:23 +01:00
griffi-gh f7d8166513 move impls into separate module 2024-03-24 01:13:20 +01:00
griffi-gh 107ccc99a9 impl from corners vec3 2024-03-24 01:10:22 +01:00
griffi-gh 65f7cc9328 fix integrate frame api...
still sucks to use
2024-03-24 00:52:37 +01:00
griffi-gh f8bf39342b document corner radius 2024-03-23 23:32:21 +01:00
griffi-gh f60087f12e add docs 2024-03-23 23:23:24 +01:00
griffi-gh 71b160d7aa frame api redesign :3 2024-03-23 23:05:54 +01:00
griffi-gh 51366210bf remove line 2024-03-23 19:55:04 +01:00
griffi-gh 769d045d60 frame api is a mess... 2024-03-23 19:53:32 +01:00
griffi-gh 03082213fb rename stuff... 2024-03-23 19:42:53 +01:00
griffi-gh 80dd442d98 fvck 2024-03-23 17:54:47 +01:00
griffi-gh 98065f46af change name function to lower case 2024-03-23 15:54:33 +01:00
griffi-gh 1b3671a3bf rename Br to Break 2024-03-23 15:52:54 +01:00
griffi-gh 101d685148 add interactable active check 2024-03-23 15:52:14 +01:00
griffi-gh 5bc048f911 use new trigger api for interactable 2024-03-23 15:50:44 +01:00
griffi-gh 32cf9dca29 add todo note 2024-03-23 15:43:22 +01:00
griffi-gh bbc4cb00e0 for now, make frame non-pub 2024-03-23 15:41:19 +01:00
griffi-gh 4ce36cc7a9 impl from corners f32 for roundedcorners 2024-03-23 15:10:32 +01:00
griffi-gh 897a574931 aaa 2024-03-23 15:09:05 +01:00
griffi-gh 15d92a2c74 uhh i'm not happy with this 2024-03-23 02:05:44 +01:00
griffi-gh 1cd2f0ec1a frame stuff 2024-03-23 01:57:07 +01:00
griffi-gh 918597585d impl from stuff 2024-03-22 19:35:00 +01:00
griffi-gh 388b71ebd0 static -> absolute, fraction -> relative 2024-03-22 18:39:18 +01:00
griffi-gh d1732fe2b7 add image to frame layer 2024-03-22 18:35:00 +01:00
griffi-gh 64640f215a frame wip 2024-03-22 18:33:34 +01:00
griffi-gh 2c099b5edd update example 2024-03-22 15:25:01 +01:00
griffi-gh 84d494f8a8 slider stuff 2024-03-22 15:15:47 +01:00
griffi-gh ccb7d22a58 fix containers defaulting to black color 2024-03-21 23:57:35 +01:00
griffi-gh fea53226a8 rename ui_test_6 2024-03-21 23:53:11 +01:00
griffi-gh 0c46aa2caf in example 6, make the text reflect the amount 2024-03-21 23:52:49 +01:00
griffi-gh ac7ec94719 add active part to slider, optimize rendering 2024-03-21 23:51:00 +01:00
griffi-gh 384fddce34 add fn to check if completely opaque 2024-03-21 23:38:43 +01:00
griffi-gh d36a78d2ee remove deprecated BackgroundColor struct 2024-03-21 23:31:49 +01:00
griffi-gh 02558e0695 Rename rectangle module to rect 2024-03-21 23:22:40 +01:00
griffi-gh 16d8fd7c6a Refactor fill color stuff 2024-03-21 23:20:47 +01:00
griffi-gh 9600d7f7a9 Deprecate bacgroundcolor, update comments 2024-03-21 23:09:19 +01:00
griffi-gh 8a0b8281bc add doc comment for slider 2024-03-21 22:59:37 +01:00
griffi-gh 1698961d5c fix some warnings 2024-03-21 22:59:31 +01:00
griffi-gh 4bd93b3106 comment out element triggers 2024-03-21 22:26:50 +01:00
griffi-gh b9aab4152f a bunch of signal and input changes 2024-03-21 22:23:42 +01:00
griffi-gh 243400ee8e compute signal in the fn body 2024-03-21 19:43:53 +01:00
griffi-gh b21184addf use with_title 2024-03-21 19:40:48 +01:00
griffi-gh c60775ff4d slider and some input stuff 2024-03-21 18:41:28 +01:00
griffi-gh 4476307482 wip slider and stuff 2024-03-14 13:30:44 +01:00
griffi-gh 7d3932f139 actually check event type 2024-03-13 14:35:12 +01:00
griffi-gh 3a58e530b3 stuff 2024-03-12 19:48:17 +01:00
griffi-gh cd338f101c remove unused deps 2024-03-12 18:37:34 +01:00
griffi-gh b185dd2bb4 update example 5 2024-03-12 18:36:47 +01:00
griffi-gh 4437eb9878 use nearest filter 2024-03-12 18:19:04 +01:00
griffi-gh d43fd91a4e uwu 2024-03-12 01:51:27 +01:00
griffi-gh 509d39ddea drain remaining signals 2024-03-12 01:40:51 +01:00
griffi-gh de9f41c296 add br element, update docs 2024-03-12 01:38:11 +01:00
griffi-gh c3f32b3ddd somewhat less hacky... 2024-03-12 01:26:48 +01:00
griffi-gh 6b2d4fbe36 well it somewhat works, build using terrible hacks 2024-03-12 01:23:26 +01:00
griffi-gh c2d7542bb8 add docs 2024-03-12 01:07:17 +01:00
griffi-gh fc458464a7 update interactable 2024-03-12 01:06:03 +01:00
griffi-gh f716433d37 signal stuff... 2024-03-12 00:29:26 +01:00
griffi-gh 4c137095a0 yeah it's a total mess 2024-03-11 21:00:43 +01:00
griffi-gh 197b327b1f make arg mut 2024-03-11 20:52:25 +01:00
griffi-gh 2d59f76ba7 it kinda works 2024-03-11 20:48:39 +01:00
griffi-gh 13785a4946 fix unused imort 2024-03-11 20:17:39 +01:00
griffi-gh dcdbc0fcbe fix warning 2024-03-11 20:16:26 +01:00
griffi-gh 67290e52ea owo 2024-03-11 20:16:01 +01:00
griffi-gh 1fe104e6f3 uwu 2024-03-11 18:53:24 +01:00
griffi-gh 5d98d7c657 add some helper fns 2024-03-11 18:48:46 +01:00
griffi-gh 352691c228 update input handling 2024-03-11 18:40:11 +01:00
griffi-gh f25efdb3ca stuff 2024-03-07 23:03:13 +01:00
griffi-gh a92817cb88 move stuff out 2024-03-07 22:46:01 +01:00
griffi-gh 8a6785b688 fancier vscode demo 2024-03-07 02:56:05 +01:00
griffi-gh af138760c9 Use non-srgb textures 2024-03-07 02:47:10 +01:00
griffi-gh 1ac9f4abd0 add wrap to vs demo 2024-03-07 02:44:04 +01:00
griffi-gh ddf093ed14 vscode demo stuff 2024-03-07 02:40:47 +01:00
griffi-gh b6cd7567d7 change stuff 2024-03-07 02:40:11 +01:00
griffi-gh 25f947ba31 add extra helpers 2024-03-07 02:24:45 +01:00
griffi-gh 788ee5e98f wip 2024-03-07 02:12:14 +01:00
griffi-gh 226fac9e8b Rename UiDirection to Direction 2024-03-07 02:06:14 +01:00
griffi-gh 481fff7dee add Text::new 2024-03-07 02:05:39 +01:00
griffi-gh 6064567433 wip image, and required ctx stuff 2024-03-07 02:04:24 +01:00
griffi-gh 688bc6e489 add todo note 2024-03-07 01:19:45 +01:00
griffi-gh fb60c25385 add title to examples 2024-03-07 01:15:33 +01:00
griffi-gh 796e274e32 rename examples 2024-03-07 01:13:43 +01:00
griffi-gh 3731f19380 apply bg image 2024-03-07 01:12:39 +01:00
griffi-gh 1fc63cf070 api and doc stuff, add bg image prop to container 2024-03-07 01:11:53 +01:00
griffi-gh f05659e35b change bg color api again 2024-03-07 00:50:47 +01:00
griffi-gh 047d53016f set_title 2024-03-07 00:42:01 +01:00
griffi-gh 677cc7d37d modernize mom downloader 2024-03-07 00:41:26 +01:00
griffi-gh 2c20da0b3b clarify issue 2024-03-06 21:46:19 +01:00
griffi-gh fe093c5db2 add hack 2024-03-06 21:35:32 +01:00
griffi-gh e2a328f4b7 add a comment to fix stuff later 2024-03-06 21:28:35 +01:00
griffi-gh 507eff3f8f use full init syntax 2024-03-06 21:28:12 +01:00
griffi-gh b9f58285da fix corrupted text bug 2024-03-06 21:14:40 +01:00
griffi-gh 4d2d947e68 fix some atlas bugs 2024-03-06 20:58:50 +01:00
griffi-gh b6898d5b79 implement element wrapping 2024-03-06 20:39:25 +01:00
griffi-gh 689541e804 add settings 2024-03-06 20:34:28 +01:00
griffi-gh 4621f0335b remove non_exhaustive from Hints, upd docs 2024-03-06 17:52:34 +01:00
griffi-gh 4c26a1297f add should_wrap flag 2024-03-06 17:19:35 +01:00
griffi-gh ef0d610764 add wrap flag 2024-03-06 17:00:18 +01:00
griffi-gh cba0795213 wrap test 2024-03-06 15:44:02 +01:00
griffi-gh 6feefb61fa remove comment 2024-03-05 23:47:56 +01:00
griffi-gh e7edd98d41 aaa 2024-03-05 23:47:40 +01:00
griffi-gh d1588932e5 fix rounded rect gradient 2024-03-02 01:19:47 +01:00
griffi-gh 6549a6a410 fix align_test example 2024-03-02 01:09:57 +01:00
griffi-gh 392fed5798 update docs 2024-03-02 01:07:53 +01:00
griffi-gh ac723bad3f oops 2024-03-02 00:35:00 +01:00
griffi-gh 4affd56d7a ui transforms 2024-03-02 00:33:02 +01:00
griffi-gh cca9890600 fix alpha 2024-03-01 20:49:41 +01:00
griffi-gh 1d48a386cb do not invalidate buffers 2024-03-01 20:49:34 +01:00
griffi-gh 80282780d5 minor comment edit 2024-03-01 20:44:37 +01:00
griffi-gh 551dd343ac upd log 2024-03-01 18:45:56 +01:00
griffi-gh a09a25ba73 idk a bunch of changes i forgor to commit 2024-03-01 18:21:02 +01:00
griffi-gh 91367d54e0 uwu 2024-02-29 23:53:01 +01:00
griffi-gh be589d302d input handling stuff 2024-02-29 17:57:06 +01:00
griffi-gh 3a8ff21189 hook up winit impl to examples 2024-02-29 16:11:29 +01:00
griffi-gh 45bbdd57fd refactor 2024-02-29 16:02:05 +01:00
griffi-gh cd589d29ae stuff 2024-02-29 02:19:29 +01:00
griffi-gh 27205a93d2 input upd (wip) 2024-02-29 01:15:29 +01:00
griffi-gh dea07fb639 Update README.md 2024-02-28 17:16:37 +01:00
griffi-gh e140075549 wait it looks wrong 2024-02-28 17:01:43 +01:00
griffi-gh 2a8c6dddb1 update readme 2024-02-28 16:54:14 +01:00
griffi-gh 661c00ab87 upd example 2024-02-28 16:47:46 +01:00
griffi-gh 3471a0c382 fix offset 2024-02-27 22:57:46 +01:00
griffi-gh b7a4b82056 i think this looks better... 2024-02-27 22:38:43 +01:00
griffi-gh 35ee24bf06 cfg gate the stuff 2024-02-27 22:18:55 +01:00
griffi-gh ba083b76f5 add color html 2024-02-27 20:31:16 +01:00
griffi-gh d86cce035b rename stuff 2024-02-27 20:31:12 +01:00
griffi-gh 9f3cc681b5 add docs, update stuff 2024-02-27 18:23:55 +01:00
griffi-gh 2c7af890eb update size macro, add docs to layout module 2024-02-27 17:56:46 +01:00
griffi-gh c726120f82 x 2024-02-26 23:49:53 +01:00
griffi-gh 8759f0169c rename arg 2024-02-26 20:04:52 +01:00
griffi-gh a36c127e61 use derive_setters 2024-02-26 16:37:59 +01:00
griffi-gh 8c5ba17c9c api stuff 2024-02-26 16:33:55 +01:00
griffi-gh 684cab5ade add checks 2024-02-26 15:19:13 +01:00
griffi-gh 0f98da2753 eh... its better i guess. still needs a refactor 2024-02-26 15:13:03 +01:00
griffi-gh 6bc912e936 HACK 2024-02-26 01:30:52 +01:00
griffi-gh 2937f8e7e3 a 2024-02-26 01:27:25 +01:00
griffi-gh 0a7392684a a 2024-02-26 01:20:52 +01:00
griffi-gh 20f89a7567 wip refactor 2024-02-26 01:15:55 +01:00
griffi-gh b616548b83 minor changes 2024-02-25 15:59:12 +01:00
griffi-gh 966bcb748f uwu 2024-02-25 15:43:38 +01:00
griffi-gh 21508c0f76 kinda works now... 2024-02-25 04:02:10 +01:00
griffi-gh 91c5b99bce wip refactor 2024-02-24 23:32:09 +01:00
griffi-gh bb76139598 x 2024-02-21 23:56:55 +01:00
griffi-gh a5cd74e911 WIP single draw call architecture 2024-02-21 20:13:58 +01:00
griffi-gh 1f7685aef5 use usize max as the default font handle (to ensure it always stays invalid) 2024-02-21 11:20:45 +01:00
griffi-gh f7366b9bbb fix builtin font handle thingy 2024-02-21 11:19:14 +01:00
griffi-gh f9ea777954 bump version to 0.1.0.alpha.4 2024-02-21 11:06:13 +01:00
griffi-gh f8d67da0c9 remove unused enum 2024-02-21 11:03:36 +01:00
griffi-gh a6b2244461 x 2024-02-20 22:45:10 +01:00
griffi-gh 7df6340531 publish 0.1.0-alpha.3 2024-02-20 21:21:58 +01:00
griffi-gh 3f60cbd751 rename border_radius to corner_radius add it to mom downloader example 2024-02-20 21:13:21 +01:00
griffi-gh 0287e46923 New background api 2024-02-20 20:56:58 +01:00
griffi-gh 3550c28cc7 fix alpha check 2024-02-20 20:31:12 +01:00
griffi-gh f95caf37e5 add todo note 2024-02-20 20:29:27 +01:00
griffi-gh fc25818e6b remove unused border code 2024-02-20 20:27:05 +01:00
griffi-gh f12a59fd31 uwu 2024-02-20 20:24:36 +01:00
griffi-gh 2fdc075f3a add border radius to progress bar 2024-02-20 20:11:05 +01:00
griffi-gh ecc137bcd5 Remoev option wrapper for bg color 2024-02-20 20:10:56 +01:00
griffi-gh 8f6a15e244 remove the need for wrapped option 2024-02-20 20:06:13 +01:00
griffi-gh 385d7edb6a allow unused parens 2024-02-20 20:03:39 +01:00
griffi-gh cbafbec066 remove hardcoded texture size 2024-02-20 19:59:39 +01:00
griffi-gh db4a6435c9 update stuff to new api, rename examples 2024-02-20 19:57:02 +01:00
griffi-gh a0eb2cd42c add Alignment2d, better docs 2024-02-20 19:48:32 +01:00
griffi-gh 23e4be05f5 document stuff 2024-02-20 18:19:10 +01:00
griffi-gh 4e6f64b60e shorter doc 2024-02-20 17:54:53 +01:00
griffi-gh af892698b5 another refactor :3 2024-02-20 17:49:44 +01:00
griffi-gh 27080e80e7 refactor stuff 2024-02-20 17:30:26 +01:00
griffi-gh 972c086a32 drop interactable api for now 2024-02-20 15:57:57 +01:00
griffi-gh 770cae8acc wip winit impl 2024-02-20 01:00:57 +01:00
griffi-gh f4a747c505 restructure stuff, add builtin_container feature 2024-02-19 23:13:35 +01:00
griffi-gh 91e8b44dd5 add pixel_perfect feature, document stuff 2024-02-19 21:32:13 +01:00
griffi-gh 99e1bc5549 refactor draw stuff 2024-02-19 21:12:12 +01:00
griffi-gh 83c00e8c13 update rounded corner api 2024-02-19 19:40:18 +01:00
griffi-gh c72a005ff6 update metadata 2024-02-19 17:49:01 +01:00
griffi-gh 81bb44f719 upd readme 2024-02-19 17:47:43 +01:00
griffi-gh ad8496279e upd readme 2024-02-19 17:35:12 +01:00
griffi-gh 5ce6f1c9f4 bump version 2024-02-19 17:27:58 +01:00
griffi-gh b267655d07 update readme 2024-02-19 17:27:16 +01:00
griffi-gh 9c3d1d9214 fix readme 2024-02-19 17:25:12 +01:00
griffi-gh 19c705866f create hui-winit 2024-02-19 17:24:31 +01:00
griffi-gh 3a045fa4c4 use url assets to make the readme work for crates 2024-02-19 17:13:06 +01:00
griffi-gh c189f412bf update readme 2024-02-19 17:10:30 +01:00
griffi-gh f82d3331a0 remove unused clip option 2024-02-19 14:44:01 +01:00
griffi-gh 6485af80ca update mom downloader example 2024-02-19 14:26:59 +01:00
griffi-gh a9edb9a142 publish version 0.1.0-alpha.2 2024-02-19 14:03:30 +01:00
griffi-gh 9a917e21e7 update rounded example 2024-02-19 14:01:51 +01:00
griffi-gh 704c2ff779 update example 2024-02-19 13:57:39 +01:00
griffi-gh e758b2459b fix center alignment with padding 2024-02-19 13:57:33 +01:00
griffi-gh e7cdc3e58e upd example 2024-02-19 13:46:13 +01:00
griffi-gh 5174421ae5 kinda.. works? 2024-02-19 05:50:46 +01:00
griffi-gh d21abc6b62 kinda works 2024-02-19 05:46:43 +01:00
griffi-gh 3e4e98eb49 corners 2024-02-19 05:36:38 +01:00
griffi-gh c50333805b wip 2024-02-19 04:37:28 +01:00
griffi-gh 4828500c45 wip rounded corners api 2024-02-19 03:41:48 +01:00
griffi-gh b04694ce83 rename stuff 2024-02-18 19:27:45 +01:00
griffi-gh 246b80c0fa fix most warnings 2024-02-18 17:22:31 +01:00
griffi-gh 57900bc287 change version track to 0.1.0-alpha.x 2024-02-18 04:06:24 +01:00
griffi-gh b65e540f0e new "context" system and text measuring 2024-02-18 04:04:02 +01:00
griffi-gh ea6623f143 Merge branch 'master' of https://github.com/griffi-gh/hui 2024-02-17 23:06:13 +01:00
griffi-gh e88c479a52 Update README.md 2024-02-17 22:56:07 +01:00
griffi-gh 4f88c7ed22 Update README.md 2024-02-17 22:53:39 +01:00
griffi-gh bb182cc74a Update README.md 2024-02-17 22:49:45 +01:00
griffi-gh 793623d088 Update README.md 2024-02-17 22:48:48 +01:00
86 changed files with 7090 additions and 1246 deletions

BIN
.assets/000000.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 B

BIN
.assets/exemplaris.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View file

@ -0,0 +1,16 @@
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"features": {
"ghcr.io/devcontainers/features/rust:1": {},
"ghcr.io/devcontainers/features/desktop-lite:1": {}
},
"forwardPorts": [6080, 5901],
"portsAttributes": {
"6080": {
"label": "desktop"
},
"5901": {
"label": "desktop"
}
}
}

5
.markdownlint.jsonc Normal file
View file

@ -0,0 +1,5 @@
{
"MD041": false, //first-line-heading
"MD013": false, //line-length
"MD033": false //inline-html
}

6
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"editor.detectIndentation": false,
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.wordWrap": "off"
}

View file

@ -1,3 +1,3 @@
[workspace]
resolver = "2"
members = ["hui", "hui-examples", "hui-glium"]
[workspace]
resolver = "2"
members = ["hui", "hui-derive", "hui-examples", "hui-glium", "hui-wgpu", "hui-winit"]

177
README.md
View file

@ -1,26 +1,167 @@
<img src="./.assets/hui.svg" width="110" align="left">
<h1>
huї
</h1>
<div align="right">
<a href="./LICENSE.txt">
<img alt="license" src="https://img.shields.io/github/license/griffi-gh/hui" align="right">
</a><br>
<a href="https://crates.io/crates/hui">
<img alt="crates.io" src="https://img.shields.io/crates/v/hui.svg" align="right">
</a>
<p></p><p></p>
<img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/hui.svg" width="120" align="left" alt="logo">
<h1>hUI</h1>
<div>
<span>
Simple UI library for games and other interactive applications
</span><a href="https://crates.io/crates/hui" float="right">
<img alt="crates.io" src="https://img.shields.io/crates/v/hui.svg?style=flat-square" align="right" height="20">
</a><br><a href="./LICENSE.txt" align="right" float="right">
<img alt="license" src="https://img.shields.io/github/license/griffi-gh/hui?style=flat-square" align="right" width="102" height="20">
</a><span>
(Formerly <code>kubi-ui</code>)
</span>
</div>
<p align="left">
Simple UI library for games and other interactive applications<br>
(formerly kubi-ui)
</p>
<p></p>
<br clear="all">
<table>
<table align="center">
<td>
<img src="./.assets/demo0.gif" width="300">
<img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/demo0.gif" width="300" alt="example: mom_downloader">
</td>
<td>
<img src="./.assets/demo1.gif" width="300">
<img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/demo1.gif" width="300" alt="example: align_test">
</td>
</table>
<h2>Example</h2>
<img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/exemplaris.png"
height="175" align="right" float="right" alt="code result">
<pre lang="rust">Container::default()
.with_size(size!(100%, 50%))
.with_align(Alignment::Center)
.with_padding(5.)
.with_gap(10.)
.with_background(rect_frame! {
color: (0.5, 0.5, 0.5, 1.),
corner_radius: 10.,
})
.with_children(|ui| {
Text::default()
.with_text("Hello, world")
.with_text_size(100)
.with_color(color::BLACK)
.add_child(ui);
Container::default()
.with_padding((10., 20.))
.with_background(rect_frame! {
color: color::DARK_RED,
corner_radius: (2.5, 30., 2.5, 2.5),
})
.with_children(|ui| {
Text::default()
.with_text("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
.with_text_size(24)
.add_child(ui);
})
.add_child(ui);
})
.add_root(ui, size);</pre>
<h2>Backends</h2>
<p>
Latest stable release:&nbsp;
<a href="https://crates.io/crates/hui" float="right">
<img alt="crates.io" src="https://img.shields.io/crates/v/hui.svg?style=flat-square&label=&color=0d1117" height="20">
</a>
</p>
<table>
<tr>
<th align="center">
<code>hui</code>
</th>
<th align="center">
<code>glium</code> (render)
</th>
<th align="center">
<code>winit</code> (platform)
</th>
<th align="center">
<code>wgpu</code> (render)
</th>
</tr>
<tr>
<td align="center">
<code>master</code>
</th>
<td>
<code>hui-glium = &lt;master&gt;</code><br>
<code>glium = "0.34"</code>
</td>
<td>
<code>hui-winit = &lt;master&gt;</code><br>
<code>winit = "0.30"</code> or <code>winit = "0.29"</code>
</td>
<td>
<code>hui-wgpu = &lt;master&gt;</code><br>
<code>wgpu = "0.20"</code>
</td>
</tr>
<tr>
<td align="center">
<code>0.1.0-alpha.4</code>
</th>
<td>
<code>hui-glium = "0.1.0-alpha.4"</code><br>
<code>glium = "0.34"</code>
</td>
<td>
<code>hui-winit = "0.1.0-alpha.4"</code><br>
<code>winit = "0.29"</code>
</td>
<td align="center">N/A</td>
</tr>
<tr>
<td align="center">
<code>0.1.0-alpha.3</code>
</th>
<td>
<code>hui-glium = "0.1.0-alpha.3"</code><br>
<code>glium = "0.34"</code>
</td>
<td align="center" colspan="2">N/A</td>
</tr>
<tr>
<td align="center">
<code>0.1.0-alpha.2</code>
</th>
<td>
<code>hui-glium = "0.1.0-alpha.2"</code><br>
<code>glium = "0.34"</code>
</td>
<td align="center" colspan="2">N/A</td>
</tr>
<tr>
<td align="center">
<code>0.1.0-alpha.1</code>
</th>
<td>
<code>hui-glium = "0.1.0-alpha.1"</code><br>
<code>glium = "0.34"</code>
</td>
<td align="center" colspan="2">N/A</td>
</tr>
<!-- <tr>
<td align="center">
<code>0.0.2</code>
</th>
<td>
<code>hui-glium = "0.0.2"</code><br>
<code>glium = "0.34"</code>
</td>
<td align="center">-</td>
</tr>
<tr>
<td align="center">
<code>0.0.1</code>
</th>
<td>
<code>hui-glium = "0.0.1"</code><br>
<code>glium = "0.34"</code>
</td>
<td align="center">-</td>
</tr> -->
</table>
<h2>MSRV</h2>
1.75

23
hui-derive/Cargo.toml Normal file
View file

@ -0,0 +1,23 @@
[package]
name = "hui-derive"
description = "Derive macros for hUI"
repository = "https://github.com/griffi-gh/hui"
readme = "../README.md"
authors = ["griffi-gh <prasol258@gmail.com>"]
rust-version = "1.75"
version = "0.1.0-alpha.5"
edition = "2021"
license = "GPL-3.0-or-later"
publish = true
include = [
"assets/**/*",
"src/**/*.rs",
"Cargo.toml",
]
[lib]
proc-macro = true
[dependencies]
quote = "1.0"
syn = "2.0"

21
hui-derive/src/lib.rs Normal file
View file

@ -0,0 +1,21 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
/// Implements `Signal` trait for the given type
#[proc_macro_derive(Signal)]
pub fn derive_signal(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
quote!(impl ::hui::signal::Signal for #name {}).into()
}
/// Implements `State` trait for the given type
#[proc_macro_derive(State)]
pub fn derive_state(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
quote!(impl ::hui::state::State for #name {}).into()
}

View file

@ -8,10 +8,12 @@ publish = false
[dev-dependencies]
hui = { path = "../hui" }
hui-glium = { path = "../hui-glium" }
hui-winit = { path = "../hui-winit", features = ["winit_29"] }
kubi-logging = { git = "https://github.com/griffi-gh/kubi", rev = "c162893fd" }
glium = "0.34"
winit = "0.29"
glam = "0.25"
glam = "0.27"
log = "0.4"
image = { version = "0.25", features = ["jpeg", "png"] }
#created as a workaround for rust-analyzer dependency cycle (which should be allowed)

Binary file not shown.

View file

@ -0,0 +1,94 @@
Copyright (c) 2015, Mew Too/Cannot Into Space Fonts (cannotintospacefonts@gmail.com),
with Reserved Font Name Blink.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -0,0 +1,2 @@
license: SIL Open Font License (OFL)
link: https://www.fontspace.com/blink-font-f21809

Binary file not shown.

View file

@ -0,0 +1,94 @@
Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.
with Reserved Font Name < Fira >,
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

View file

@ -0,0 +1,81 @@
use glam::{UVec2, Vec2};
use glium::{Surface, backend::glutin::SimpleWindowBuilder};
use winit::{
event::{Event, WindowEvent},
event_loop::{EventLoopBuilder, ControlFlow}
};
use hui::UiInstance;
use hui_glium::GliumUiRenderer;
/// Generates a `main` function that initializes glium renderer, `UiInstance`, and runs the event loop.
macro_rules! ui_main {
($name:literal, init: $closure0:expr, run: $closure1:expr) => {
fn main() {
$crate::boilerplate::ui($closure0, $closure1, $name);
}
};
(init: $closure0:expr, run: $closure1:expr) => {
fn main() {
$crate::boilerplate::ui($closure0, $closure1, "hUI example");
}
};
($closure: expr) => {
fn main() {
$crate::boilerplate::ui(|_|(), $closure, "hUI example");
}
};
}
/// Initializes glium renderer, `UiInstance`, and runs the event loop.
pub fn ui<T>(
mut init: impl FnMut(&mut UiInstance) -> T,
mut draw: impl FnMut(&mut UiInstance, Vec2, &mut T),
name: &'static str
) {
kubi_logging::init();
let event_loop = EventLoopBuilder::new().build().unwrap();
let (window, display) = SimpleWindowBuilder::new()
.with_title(name)
.build(&event_loop);
let mut hui = UiInstance::new();
let mut backend = GliumUiRenderer::new(&display);
let mut result = init(&mut hui);
event_loop.run(|event, window_target| {
window.request_redraw();
window_target.set_control_flow(ControlFlow::Poll);
hui_winit::handle_winit_event(&mut hui, &event);
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::CloseRequested => {
window_target.exit();
},
WindowEvent::RedrawRequested => {
let mut frame = display.draw();
frame.clear_color_srgb(0.5, 0.5, 0.5, 1.);
hui.begin();
let size = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
draw(&mut hui, size, &mut result);
hui.end();
backend.update(&hui);
backend.draw(&mut frame, size);
frame.finish().unwrap();
},
_ => (),
},
Event::Suspended => {
#[cfg(target_os = "android")]
window_target.exit();
},
_ => (),
}
}).unwrap();
}

View file

@ -0,0 +1,148 @@
//WARNING: THIS EXAMPLE IS EXTREMELY OUTDATED AND USES DEPRECATED API
use std::time::Instant;
use glam::{UVec2, vec4};
use glium::{backend::glutin::SimpleWindowBuilder, Surface};
use winit::{
event::{Event, WindowEvent},
event_loop::{EventLoopBuilder, ControlFlow}
};
use hui::{
element::{
container::Container, frame_view::FrameView, progress_bar::ProgressBar, ElementList, UiElement
}, frame::RectFrame, layout::{Alignment, Direction, Size}, rect::{Corners, Sides}, UiInstance
};
use hui_glium::GliumUiRenderer;
fn main() {
kubi_logging::init();
let event_loop = EventLoopBuilder::new().build().unwrap();
let (_window, display) = SimpleWindowBuilder::new().build(&event_loop);
let mut hui = UiInstance::new();
let mut backend = GliumUiRenderer::new(&display);
let instant = Instant::now();
event_loop.run(|event, window_target| {
window_target.set_control_flow(ControlFlow::Poll);
match event {
Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => {
window_target.exit();
},
Event::AboutToWait => {
let mut frame = display.draw();
frame.clear_color_srgb(0.5, 0.5, 0.5, 0.);
let resolution = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
hui.begin();
let z = instant.elapsed().as_secs_f32().sin().powi(2);
hui.add(Container {
gap: 5.,
padding: Sides::all(5.),
align: (Alignment::Center, Alignment::Begin).into(),
size: (Size::Relative(1.), Size::Relative(1.)).into(),
children: ElementList(vec![
Box::new(ProgressBar {
value: 0.5,
..Default::default()
}),
]),
..Default::default()
}, resolution);
hui.add(Container {
gap: 5.,
padding: Sides::all(5.),
align: (Alignment::Center, Alignment::End).into(),
size: (Size::Relative(1.), Size::Relative(1.)).into(),
children: ElementList(vec![
Box::new(ProgressBar {
value: z,
// corner_radius: Corners::all(0.25 * ProgressBar::DEFAULT_HEIGHT),
..Default::default()
}),
Box::new(Container {
size: (Size::Relative(1.), Size::Auto).into(),
align: (Alignment::End, Alignment::Center).into(),
padding: Sides::all(5.),
gap: 10.,
children: ElementList(vec![
Box::new(FrameView {
size: (Size::Relative(0.5), Size::Absolute(30.)).into(),
frame: Box::new(vec4(0.75, 0., 0., 1.)),
}),
Box::new(FrameView {
size: (Size::Relative(z / 2. + 0.5), Size::Absolute(30.)).into(),
frame: Box::new(Corners::left_right(
vec4(1., 0., 0., 1.),
vec4(0., 1., 0., 1.)
)),
}),
]),
..Default::default()
}),
Box::new(FrameView {
size: (Size::Relative(z / 2. + 0.5), Size::Absolute(30.)).into(),
frame: Box::new(vec4(0., 0.75, 0., 1.)),
}),
Box::new(Container {
gap: 5.,
padding: Sides::all(5.),
background_frame: Box::new(RectFrame::color(vec4(0., 0., 0., 0.5))),
direction: Direction::Horizontal,
children: {
let mut x: Vec<Box<dyn UiElement>> = vec![];
for i in 0..10 {
x.push(Box::new(FrameView {
size: (Size::Absolute(50.), Size::Absolute(50.)).into(),
frame: Box::new(if i == 1 {
vec4(0.75, 0.75, 0.75, 0.75)
} else {
vec4(0.5, 0.5, 0.5, 0.75)
}),
}));
}
ElementList(x)
},
..Default::default()
}),
Box::new(Container {
background_frame: Box::new(RectFrame::color((1., 0., 0.)).with_corner_radius(Corners {
top_left: 0.,
top_right: 30.,
bottom_left: 0.,
bottom_right: 0.,
})),
padding: Sides {
top: 10.,
bottom: 20.,
left: 30.,
right: 40.,
},
children: ElementList(vec![
Box::new(FrameView {
size: (Size::Absolute(50.), Size::Absolute(50.)).into(),
frame: Box::new(vec4(1., 1., 1., 0.75)),
}),
]),
..Default::default()
}),
]),
..Default::default()
}, resolution);
hui.end();
backend.update(&hui);
backend.draw(&mut frame, resolution);
frame.finish().unwrap();
}
_ => (),
}
}).unwrap();
}

View file

@ -1,111 +1,84 @@
use std::time::Instant;
use glam::{UVec2, vec4};
use glium::{backend::glutin::SimpleWindowBuilder, Surface};
use winit::{
event::{Event, WindowEvent},
event_loop::{EventLoopBuilder, ControlFlow}
};
use hui::{
UiInstance,
element::{
color, element::{
container::Container,
progress_bar::ProgressBar,
container::{Container, Sides, Alignment},
text::Text
},
UiSize,
elements,
text::Text,
UiElementExt,
}, frame::RectFrame, rect_frame, layout::{Alignment, Direction}, size
};
use hui_glium::GliumUiRenderer;
fn main() {
kubi_logging::init();
#[path = "../boilerplate.rs"]
#[macro_use]
mod boilerplate;
let event_loop = EventLoopBuilder::new().build().unwrap();
let (window, display) = SimpleWindowBuilder::new().build(&event_loop);
window.set_title("Mom Downloader 2000");
ui_main!{
"Mom downloader 2000",
init: |ui| {
let font_handle = ui.add_font(include_bytes!("../assets/roboto/Roboto-Regular.ttf"));
ui.push_font(font_handle);
Instant::now()
},
run: |ui, max_size, instant| {
let mom_ratio = (instant.elapsed().as_secs_f32() / 60.).powf(0.5);
let mut hui = UiInstance::new();
let mut backend = GliumUiRenderer::new(&display);
let font_handle = hui.add_font_from_bytes(include_bytes!("../assets/roboto/Roboto-Regular.ttf"));
let instant = Instant::now();
event_loop.run(|event, window_target| {
window_target.set_control_flow(ControlFlow::Poll);
match event {
Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => {
window_target.exit();
},
Event::AboutToWait => {
let mut frame = display.draw();
frame.clear_color_srgb(0., 0., 0., 1.);
let resolution = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
hui.begin();
let mom_ratio = (instant.elapsed().as_secs_f32() / 60.).powf(0.5);
hui.add(Container {
align: (Alignment::Center, Alignment::Center),
size: (UiSize::Percentage(1.), UiSize::Percentage(1.)),
background: Some(vec4(0.1, 0.1, 0.1, 1.)),
elements: vec![Box::new(Container {
gap: 5.,
padding: Sides::all(10.),
align: (Alignment::Begin, Alignment::Begin),
size: (UiSize::Pixels(450.), UiSize::Auto),
background: Some(vec4(0.2, 0.2, 0.5, 1.)),
elements: elements(|el| {
if instant.elapsed().as_secs_f32() < 5. {
el.add(Text {
text: "Downloading your mom...".into(),
font: font_handle,
text_size: 32,
..Default::default()
});
el.add(ProgressBar {
value: mom_ratio,
..Default::default()
});
el.add(Text {
text: format!("{:.2}% ({:.1} GB)", mom_ratio * 100., mom_ratio * 10000.).into(),
font: font_handle,
text_size: 24,
..Default::default()
});
} else if instant.elapsed().as_secs() < 10 {
el.add(Text {
text: "Error 413 Request Entity Too Large".into(),
font: font_handle,
color: vec4(1., 0.125, 0.125, 1.),
text_size: 26,
..Default::default()
});
el.add(Text {
text: format!("Exiting in {}...", 10 - instant.elapsed().as_secs()).into(),
font: font_handle,
text_size: 24,
..Default::default()
Container::default()
.with_align(Alignment::Center)
.with_size(size!(100%))
.with_background((0.1, 0.1, 0.1))
.with_children(|ui| {
Container::default()
.with_gap(5.)
.with_padding(10.)
.with_size(size!(450, auto))
.with_background(rect_frame! {
color: (0.2, 0.2, 0.5),
corner_radius: 8.
})
.with_children(|ui| {
if instant.elapsed().as_secs_f32() < 5. {
Text::default()
.with_text("Downloading your mom...")
.with_text_size(24)
.add_child(ui);
ProgressBar::default()
.with_value(mom_ratio)
.with_background(rect_frame! {
color: color::BLACK,
corner_radius: 0.125 * ProgressBar::DEFAULT_HEIGHT
})
} else {
window_target.exit();
}
}),
..Default::default()
})],
..Default::default()
}, resolution);
hui.end();
backend.update(&hui);
backend.draw(&mut frame, resolution);
frame.finish().unwrap();
}
_ => (),
}
}).unwrap();
.with_foreground(rect_frame! {
color: color::BLUE,
corner_radius: 0.125 * ProgressBar::DEFAULT_HEIGHT
})
.add_child(ui);
Container::default()
.with_direction(Direction::Horizontal)
.with_align((Alignment::End, Alignment::Center))
.with_size(size!(100%, auto))
.with_children(|ui| {
Text::default()
.with_text(format!("{:.2}% ({:.1} GB)", mom_ratio * 100., mom_ratio * 10000.))
.with_text_size(16)
.add_child(ui);
})
.add_child(ui);
} else if instant.elapsed().as_secs() < 10 {
Text::default()
.with_text("Error 413: Request Entity Too Large")
.with_color((1., 0.125, 0.125, 1.))
.with_text_size(20)
.add_child(ui);
Text::default()
.with_text(format!("Exiting in {}...", 10 - instant.elapsed().as_secs()))
.with_text_size(16)
.add_child(ui);
} else {
std::process::exit(0);
}
})
.add_child(ui);
})
.add_root(ui, max_size)
}
}

View file

@ -1,90 +0,0 @@
use std::time::Instant;
use glam::{Vec2, IVec2, UVec2};
use glium::{backend::glutin::SimpleWindowBuilder, Surface};
use winit::{
event::{Event, WindowEvent},
event_loop::{EventLoopBuilder, ControlFlow}
};
use hui::{
UiInstance,
element::{
UiElement,
progress_bar::ProgressBar,
container::{Container, Sides}
},
UiSize
};
use hui_glium::GliumUiRenderer;
fn main() {
kubi_logging::init();
let event_loop = EventLoopBuilder::new().build().unwrap();
let (window, display) = SimpleWindowBuilder::new().build(&event_loop);
let mut hui = UiInstance::new();
let mut backend = GliumUiRenderer::new(&display);
let instant = Instant::now();
let mut pcnt = 0;
event_loop.run(|event, window_target| {
window_target.set_control_flow(ControlFlow::Poll);
match event {
Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => {
window_target.exit();
},
Event::AboutToWait => {
let mut frame = display.draw();
frame.clear_color_srgb(0.5, 0.5, 0.5, 0.);
let resolution = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
hui.begin();
hui.add(Container {
gap: 5.,
padding: Sides::all(5.),
elements: vec![
Box::new(ProgressBar {
value: 0.5,
..Default::default()
}),
Box::new(ProgressBar {
value: instant.elapsed().as_secs_f32().sin().powi(2),
..Default::default()
}),
Box::new(Container {
gap: 1.,
elements: {
let mut elements: Vec<Box<dyn UiElement>> = vec![];
let cnt = instant.elapsed().as_secs() * 10000;
if pcnt != cnt {
log::info!("{cnt}");
pcnt = cnt;
}
for i in 0..cnt {
elements.push(Box::new(ProgressBar {
value: (instant.elapsed().as_secs_f32() + (i as f32 / 10.)).sin().powi(2),
size: (UiSize::Auto, UiSize::Pixels(5.)),
..Default::default()
}));
}
elements
},
..Default::default()
})
],
..Default::default()
}, resolution);
hui.end();
backend.update(&hui);
backend.draw(&mut frame, resolution);
frame.finish().unwrap();
}
_ => (),
}
}).unwrap();
}

View file

@ -1,146 +0,0 @@
use std::time::Instant;
use glam::{UVec2, vec4};
use glium::{backend::glutin::SimpleWindowBuilder, Surface};
use winit::{
event::{Event, WindowEvent},
event_loop::{EventLoopBuilder, ControlFlow}
};
use hui::{
UiInstance,
element::{
UiElement,
progress_bar::ProgressBar,
container::{Container, Sides, Alignment},
rect::Rect
},
interaction::IntoInteractable,
UiSize,
UiDirection, IfModified,
};
use hui_glium::GliumUiRenderer;
fn main() {
kubi_logging::init();
let event_loop = EventLoopBuilder::new().build().unwrap();
let (window, display) = SimpleWindowBuilder::new().build(&event_loop);
let mut hui = UiInstance::new();
let mut backend = GliumUiRenderer::new(&display);
let instant = Instant::now();
event_loop.run(|event, window_target| {
window_target.set_control_flow(ControlFlow::Poll);
match event {
Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => {
window_target.exit();
},
Event::AboutToWait => {
let mut frame = display.draw();
frame.clear_color_srgb(0.5, 0.5, 0.5, 0.);
let resolution = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
hui.begin();
let z = instant.elapsed().as_secs_f32().sin().powi(2);
hui.add(Container {
gap: 5.,
padding: Sides::all(5.),
align: (Alignment::Begin, Alignment::Center),
size: (UiSize::Percentage(1.), UiSize::Percentage(1.)),
elements: vec![
Box::new(ProgressBar {
value: 0.5,
..Default::default()
}),
],
..Default::default()
}, resolution);
hui.add(Container {
gap: 5.,
padding: Sides::all(5.),
align: (Alignment::End, Alignment::Center),
size: (UiSize::Percentage(1.), UiSize::Percentage(1.)),
elements: vec![
Box::new(ProgressBar {
value: z,
..Default::default()
}),
Box::new(Container {
size: (UiSize::Percentage(1.), UiSize::Auto),
align: (Alignment::Center, Alignment::End),
padding: Sides::all(5.),
gap: 10.,
elements: vec![
Box::new(Rect {
size: (UiSize::Percentage(0.5), UiSize::Pixels(30.)),
color: Some(vec4(0.75, 0., 0., 1.))
}),
Box::new(Rect {
size: (UiSize::Percentage(z / 2. + 0.5), UiSize::Pixels(30.)),
color: Some(vec4(0., 0.75, 0., 1.))
}),
],
..Default::default()
}),
Box::new(Rect {
size: (UiSize::Percentage(z / 2. + 0.5), UiSize::Pixels(30.)),
color: Some(vec4(0., 0.75, 0., 1.))
}),
Box::new(Container {
gap: 5.,
padding: Sides::all(5.),
background: Some(vec4(0., 0., 0., 0.5)),
direction: UiDirection::Horizontal,
elements: {
let mut x: Vec<Box<dyn UiElement>> = vec![];
for i in 0..10 {
x.push(Box::new(Rect {
size: (UiSize::Pixels(50.), UiSize::Pixels(50.)),
color: if i == 1 {
Some(vec4(0.75, 0.75, 0.75, 0.75))
} else {
Some(vec4(0.5, 0.5, 0.5, 0.75))
}
}));
}
x
},
..Default::default()
}),
Box::new(Container {
background: Some(vec4(1., 0., 0., 1.)),
padding: Sides {
top: 10.,
bottom: 20.,
left: 30.,
right: 40.,
},
elements: vec![
Box::new(Rect {
size: (UiSize::Pixels(50.), UiSize::Pixels(50.)),
color: Some(vec4(1., 1., 1., 0.75))
}.into_interactable().on_click(|| {
println!("clicked");
}))
],
..Default::default()
})
],
..Default::default()
}, resolution);
hui.end();
backend.update(&hui);
backend.draw(&mut frame, resolution);
frame.finish().unwrap();
}
_ => (),
}
}).unwrap();
}

View file

@ -1,3 +1,5 @@
//WARNING: THIS EXAMPLE IS EXTREMELY OUTDATED AND USES DEPRECATED API
use std::time::Instant;
use glam::{UVec2, vec4};
use glium::{backend::glutin::SimpleWindowBuilder, Surface};
@ -6,16 +8,18 @@ use winit::{
event_loop::{EventLoopBuilder, ControlFlow}
};
use hui::{
UiInstance,
element::{
container::Container,
text::Text, rect::Rect, spacer::Spacer
},
UiSize,
elements,
container::Container, frame_view::FrameView, spacer::Spacer, text::Text, ElementList
}, frame::RectFrame, layout::Size, UiInstance
};
use hui_glium::GliumUiRenderer;
fn elements(mut f: impl FnMut(&mut Vec<Box<dyn hui::element::UiElement>>)) -> ElementList {
let mut e = vec![];
f(&mut e);
ElementList(e)
}
fn main() {
kubi_logging::init();
@ -26,7 +30,7 @@ fn main() {
let mut hui = UiInstance::new();
let mut backend = GliumUiRenderer::new(&display);
let font_handle = hui.add_font_from_bytes(include_bytes!("../assets/roboto/Roboto-Regular.ttf"));
let font_handle = hui.add_font(include_bytes!("../assets/roboto/Roboto-Regular.ttf"));
let instant = Instant::now();
event_loop.run(|event, window_target| {
@ -44,59 +48,59 @@ fn main() {
hui.begin();
hui.add(Container {
size: (UiSize::Percentage(1.), UiSize::Percentage(1.)),
background: Some(vec4(0.1, 0.1, 0.1, 1.)),
elements: elements(|elem| {
elem.add(Text {
size: (Size::Relative(1.), Size::Relative(1.)).into(),
background_frame: Box::new(RectFrame::color((0.1, 0.1, 0.1, 1.))),
children: elements(|elem| {
elem.push(Box::new(Text {
text: "THIS LINE SHOULD BE SHARP!".into(),
..Default::default()
});
elem.add(Text {
}));
elem.push(Box::new(Text {
text: "THIS LINE SHOULD BE SHARP!".into(),
text_size: 32,
..Default::default()
});
elem.add(Text {
}));
elem.push(Box::new(Text {
text: "All lines except 3 and 6 below will be blurry:".into(),
..Default::default()
});
}));
for size in [9, 12, 16, 18, 24, 32] {
elem.add(Text {
elem.push(Box::new(Text {
text: "Testing default font, Proggy Tiny".into(),
text_size: size,
..Default::default()
});
}));
}
elem.add(Rect {
size: (UiSize::Percentage(1.), UiSize::Pixels(10.)),
color: Some(vec4(0., 0., 1., 1.)),
});
elem.add(Rect {
size: (UiSize::Percentage(1.), UiSize::Pixels(10.)),
color: Some(vec4(1., 1., 0., 1.)),
});
elem.add(Text {
elem.push(Box::new(FrameView {
size: (Size::Relative(1.), Size::Absolute(10.)).into(),
frame: Box::new(vec4(0., 0., 1., 1.)),
}));
elem.push(Box::new(FrameView {
size: (Size::Relative(1.), Size::Absolute(10.)).into(),
frame: Box::new(vec4(1., 1., 0., 1.)),
}));
elem.push(Box::new(Text {
text: "Hello, world!\nżółty liść. życie nie ma sensu i wszyscy zginemy;\nтест кирилиці їїїїїїїїїїї\njapanese text: テスト".into(),
font: font_handle,
font: Some(font_handle),
text_size: 32,
..Default::default()
});
}));
if instant.elapsed().as_secs() & 1 != 0 {
elem.add(Rect {
size: (UiSize::Percentage(1.), UiSize::Pixels(10.)),
color: Some(vec4(1., 0., 0., 1.)),
});
elem.add(Rect {
size: (UiSize::Percentage(1.), UiSize::Pixels(10.)),
color: Some(vec4(0., 0., 0., 1.)),
});
elem.add(Spacer(100.));
elem.add(Text {
elem.push(Box::new(FrameView {
size: (Size::Relative(1.), Size::Absolute(10.)).into(),
frame: Box::new(vec4(1., 0., 0., 1.)),
}));
elem.push(Box::new(FrameView {
size: (Size::Relative(1.), Size::Absolute(10.)).into(),
frame: Box::new(vec4(0., 0., 0., 1.)),
}));
elem.push(Box::new(Spacer(100.)));
elem.push(Box::new(Text {
text: "FLAG SHOULD NOT OVERLAP WITH TEXT".into(),
text_size: 64,
color: vec4(1., 0., 1., 1.),
..Default::default()
});
}));
}
}),
..Default::default()

View file

@ -0,0 +1,43 @@
use hui::{
color, size, rect_frame,
element::{container::Container, text::Text, UiElementExt},
frame::RectFrame,
layout::Alignment,
};
#[path = "../boilerplate.rs"]
#[macro_use]
mod boilerplate;
ui_main!(|ui, size, _| {
Container::default()
.with_size(size!(100%, 50%))
.with_align(Alignment::Center)
.with_padding(5.)
.with_gap(10.)
.with_background(rect_frame! {
color: (0.5, 0.5, 0.5, 1.),
corner_radius: 10.,
})
.with_children(|ui| {
Text::default()
.with_text("Hello, world")
.with_text_size(100)
.with_color(color::BLACK)
.add_child(ui);
Container::default()
.with_padding((10., 20.))
.with_background(rect_frame! {
color: color::DARK_RED,
corner_radius: (2.5, 30., 2.5, 2.5),
})
.with_children(|ui| {
Text::default()
.with_text("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
.with_text_size(24)
.add_child(ui);
})
.add_child(ui);
})
.add_root(ui, size);
});

View file

@ -0,0 +1,133 @@
use glam::vec4;
use hui::{
size, rect_frame,
color,
element::{
container::Container,
progress_bar::ProgressBar,
text::Text,
UiElementExt
},
frame::RectFrame,
layout::Alignment,
rect::Corners,
text::FontHandle
};
#[path = "../boilerplate.rs"]
#[macro_use]
mod boilerplate;
ui_main!(
"hUI: Loading screen demo",
init: |ui| {
let font = ui.add_font(include_bytes!("../assets/blink/Blink-ynYZ.otf"));
ui.push_font(font);
(std::time::Instant::now(),)
},
run: |ui, size, (instant,)| {
// Background color (gradient)
Container::default()
.with_size(size!(100%))
.with_background(Corners {
top_left: vec4(0.2, 0.2, 0.3, 1.),
top_right: vec4(0.3, 0.3, 0.4, 1.),
bottom_left: vec4(0.2, 0.3, 0.2, 1.),
bottom_right: vec4(0.5, 0.4, 0.4, 1.),
})
.add_root(ui, size);
// Loading text in the bottom right corner
Container::default()
.with_size(size!(100%))
.with_align(Alignment::End)
.with_padding(20.)
.with_children(|ui| {
Container::default()
.with_padding((10., 15.))
.with_background(rect_frame! {
color: (0., 0., 0., 0.5),
corner_radius: 8.,
})
.with_children(|ui| {
let flash = 1. - 0.5 * (4. * instant.elapsed().as_secs_f32()).sin().powi(2);
Text::default()
.with_text("Loading...")
.with_color((1., 1., 1., flash))
.with_text_size(24)
.add_child(ui);
})
.add_child(ui);
})
.add_root(ui, size);
// Did you know? box in the center
Container::default()
.with_size(size!(100%))
.with_align(Alignment::Center)
.with_children(|ui| {
Container::default()
.with_align((Alignment::Center, Alignment::Begin))
.with_padding(15.)
.with_gap(10.)
.with_background(rect_frame! {
color: (0., 0., 0., 0.5),
corner_radius: 8.,
})
.with_children(|ui| {
Text::default()
.with_text("Did you know?")
.with_text_size(18)
.add_child(ui);
Text::default()
.with_text("You can die by jumping into the spike pit! :D\nCheck out the tutorial section for more tips.")
.with_text_size(24)
.with_font(FontHandle::default())
.add_child(ui);
})
.add_child(ui);
})
.add_root(ui, size);
// Progress bar at the bottom
Container::default()
.with_size(size!(100%))
.with_align((Alignment::Center, Alignment::End))
.with_children(|ui| {
ProgressBar::default()
.with_value((instant.elapsed().as_secs_f32() * 0.1) % 1.)
.with_size(size!(100%, 5))
.with_background((0., 0., 0., 0.5))
.with_foreground(color::DARK_GREEN)
.add_child(ui);
})
.add_root(ui, size);
// Player XP and level (mock) in the top right corner
Container::default()
.with_size(size!(100%))
.with_align((Alignment::End, Alignment::Begin))
.with_padding(20.)
.with_children(|ui| {
Container::default()
.with_padding(10.)
.with_background(rect_frame!{
color: (0., 0., 0., 0.5),
corner_radius: 8.,
})
.with_children(|ui| {
Text::default()
.with_text("Level 5")
.with_text_size(24)
.add_child(ui);
Text::default()
.with_text("XP: 1234 / 5000")
.with_text_size(18)
.with_font(FontHandle::default())
.add_child(ui);
})
.add_child(ui);
})
.add_root(ui, size);
}
);

View file

@ -0,0 +1,62 @@
use std::f32::consts::PI;
use glam::{vec4, Vec2};
use hui::{
element::{
container::Container,
text::Text,
transformer::ElementTransformExt,
UiElementExt
}, frame::RectFrame, rect_frame, layout::Alignment, rect::Corners, size, text::FontHandle
};
#[path = "../boilerplate.rs"]
#[macro_use]
mod boilerplate;
ui_main!(
"hUI: Transform API demo",
init: |ui| {
let font = ui.add_font(include_bytes!("../assets/blink/Blink-ynYZ.otf"));
ui.push_font(font);
(std::time::Instant::now(),)
},
run: |ui, size, (instant,)| {
let elapsed_sec = instant.elapsed().as_secs_f32();
Container::default()
.with_background(Corners {
top_left: vec4(0.2, 0.2, 0.3, 1.),
top_right: vec4(0.3, 0.3, 0.4, 1.),
bottom_left: vec4(0.2, 0.3, 0.2, 1.),
bottom_right: vec4(0.5, 0.4, 0.4, 1.),
})
.with_size(size!(100%))
.with_align(Alignment::Center)
.with_children(|ui| {
Container::default()
.with_align((Alignment::Center, Alignment::Begin))
.with_padding(15.)
.with_gap(10.)
.with_background(rect_frame! {
color: (0., 0., 0., 0.5),
corner_radius: 8.
})
.with_children(|ui| {
Text::default()
.with_text("Did you know?")
.with_text_size(18)
.add_child(ui);
Text::default()
.with_text("You can die by jumping into the spike pit! :D\nCheck out the tutorial section for more tips.")
.with_text_size(24)
.with_font(FontHandle::default())
.add_child(ui);
})
.transform()
.scale(Vec2::splat(elapsed_sec.sin() * 0.1 + 1.))
.rotate(elapsed_sec * PI / 4.)
.add_child(ui);
})
.add_root(ui, size);
}
);

View file

@ -0,0 +1,42 @@
use std::time::Instant;
use hui::{
color, element::{
container::Container,
frame_view::FrameView,
UiElementExt
}, rect_frame, layout::{Alignment, Direction}, size
};
#[path = "../boilerplate.rs"]
#[macro_use]
mod boilerplate;
ui_main!(
"hUI: Wrapping demo",
init: |_| {
Instant::now()
},
run: |ui, size, instant| {
let width_ratio = 0.5 + 0.5 * instant.elapsed().as_secs_f32().sin().powi(2);
Container::default()
.with_size(size!(width_ratio/, 100%))
.with_direction(Direction::Horizontal)
.with_align(Alignment::Center)
.with_padding(5.)
.with_gap(10.)
.with_background(color::WHITE)
.with_wrap(true)
.with_children(|ui| {
for i in 0..10 {
FrameView::default()
.with_size(size!((40 + i * 10)))
.with_frame(rect_frame! {
color: color::DARK_RED,
corner_radius: 8.
})
.add_child(ui);
}
})
.add_root(ui, size);
}
);

View file

@ -0,0 +1,91 @@
use hui::{
color, size,
signal::Signal,
draw::TextureFormat,
layout::{Alignment, Direction},
element::{
container::Container,
text::Text,
image::Image,
br::Break,
interactable::ElementInteractableExt,
UiElementExt,
},
};
#[derive(Signal)]
enum CounterSignal {
Increment,
Decrement,
}
#[path = "../boilerplate.rs"]
#[macro_use]
mod boilerplate;
const IMAGE_DATA: &[u8] = include_bytes!("../assets/icons/visual-studio-code-icon_32x32.rgba");
ui_main!(
"hUI: Internal input test",
init: |ui| {
let image = ui.add_image(TextureFormat::Rgba, IMAGE_DATA, 32);
(0, image)
},
run: |ui, size, (ref mut counter, image)| {
Container::default()
.with_size(size!(100%))
.with_padding(10.)
.with_align((Alignment::Center, Alignment::Begin))
.with_direction(Direction::Horizontal)
.with_gap(5.)
.with_background((0.1, 0.1, 0.1))
.with_wrap(true)
.with_children(|ui| {
Text::new("Number of images:")
.with_text_size(24)
.add_child(ui);
Break.add_child(ui);
Container::default()
.with_padding(10.)
.with_background(color::ORANGE)
.with_children(|ui| {
Text::new("-")
.with_text_size(32)
.add_child(ui);
})
.on_click(|| CounterSignal::Decrement)
.add_child(ui);
Container::default()
.with_size(size!(60, auto))
.with_align(Alignment::Center)
.with_children(|ui| {
Text::new(counter.to_string())
.with_text_size(64)
.add_child(ui);
})
.add_child(ui);
Container::default()
.with_padding(10.)
.with_background(color::ORANGE)
.with_children(|ui| {
Text::new("+")
.with_text_size(32)
.add_child(ui);
})
.on_click(|| CounterSignal::Increment)
.add_child(ui);
Break.add_child(ui);
for _ in 0..*counter {
Image::new(*image)
.with_size(size!(48, 48))
.add_child(ui);
}
})
.add_root(ui, size);
ui.process_signals(|sig| match sig {
CounterSignal::Increment => *counter += 1,
CounterSignal::Decrement => *counter -= 1,
});
}
);

View file

@ -0,0 +1,82 @@
use hui::{
draw::TextureFormat,
element::{
br::Break,
container::Container,
image::Image,
slider::Slider,
text::Text,
UiElementExt,
},
signal::Signal,
layout::{Alignment, Direction},
size,
};
#[derive(Signal)]
enum CounterSignal {
ChangeValue(u32)
}
#[path = "../boilerplate.rs"]
#[macro_use]
mod boilerplate;
const IMAGE_DATA: &[u8] = include_bytes!("../assets/icons/visual-studio-code-icon_32x32.rgba");
ui_main!(
"hUI: Internal input test",
init: |ui| {
let image = ui.add_image(TextureFormat::Rgba, IMAGE_DATA, 32);
(0, image)
},
run: |ui, size, (ref mut counter, image)| {
Container::default()
.with_size(size!(100%))
.with_padding(10.)
.with_align((Alignment::Center, Alignment::Begin))
.with_direction(Direction::Horizontal)
.with_gap(5.)
.with_background((0.1, 0.1, 0.1))
.with_wrap(true)
.with_children(|ui| {
Text::new(format!("Number of images: {counter}"))
.with_text_size(32)
.add_child(ui);
Break.add_child(ui);
Text::new("Absolute tracking slider:")
.with_text_size(16)
.add_child(ui);
Break.add_child(ui);
Slider::new(*counter as f32 / 100.)
.with_size(size!(66%, 20))
.on_change(|x| {
CounterSignal::ChangeValue((x * 100.).round() as u32)
})
.add_child(ui);
Break.add_child(ui);
Text::new("Relative tracking slider (Experimental):")
.with_text_size(16)
.add_child(ui);
Break.add_child(ui);
Slider::new(*counter as f32 / 100.)
.with_size(size!(66%, 20))
.with_follow_mode(hui::element::slider::SliderFollowMode::Relative)
.on_change(|x| {
CounterSignal::ChangeValue((x * 100.).round() as u32)
})
.add_child(ui);
Break.add_child(ui);
for _ in 0..*counter {
Image::new(*image)
.with_size(size!(48, 48))
.add_child(ui);
}
})
.add_root(ui, size);
ui.process_signals(|sig| match sig {
CounterSignal::ChangeValue(v) => *counter = v,
});
}
);

View file

@ -0,0 +1,92 @@
use glam::vec2;
use hui::{
color,
element::{
container::Container,
frame_view::FrameView,
slider::Slider,
text::Text,
UiElementExt
},
frame::nine_patch::{NinePatchAsset, NinePatchFrame},
layout::Alignment,
rect::Rect,
signal::Signal,
size,
};
#[path = "../boilerplate.rs"]
#[macro_use]
mod boilerplate;
#[derive(Signal)]
struct SetValue(f32);
ui_main!(
"hUI: 9-Patch demo",
init: |ui| {
(
NinePatchAsset {
image: ui.add_image_file_path("./hui-examples/assets/ninepatch_button.png").unwrap(),
size: (190, 49),
scalable_region: Rect {
position: vec2(8. / 190., 8. / 49.),
size: vec2(1. - 16. / 190., 1. - 18. / 49.),
},
},
0.33,
)
},
run: |ui, size, (asset, value)| {
Container::default()
.with_size(size!(100%))
.with_align(Alignment::Center)
.with_gap(5.)
.with_background(color::WHITE)
.with_children(|ui| {
Container::default()
.with_size(size!(300, 100))
.with_background(NinePatchFrame::from_asset(*asset).with_color(color::RED))
.with_padding(10.)
.with_children(|ui| {
Text::new("Hello, world!\nThis is a 9-patch frame used as a background \nfor Container with a Text element.\nIt's scalable and looks great!\nBelow, there are two FillRects with the same \n9-patch frame used as the background.")
.with_text_size(16)
.add_child(ui);
})
.add_child(ui);
FrameView::default()
.with_size(size!(600, 75))
.with_frame(NinePatchFrame::from_asset(*asset).with_color(color::GREEN))
.add_child(ui);
Text::new("This one's fancy:")
.with_color(color::BLACK)
.with_text_size(32)
.add_child(ui);
FrameView::default()
.with_size(size!(700, 50))
.with_frame(NinePatchFrame::from_asset(*asset).with_color((
(1., 0., 1.),
(0., 1., 1.),
(1., 1., 0.),
(0., 0., 1.),
)))
.add_child(ui);
Text::new("Slider customized with `NinePatchFrame`s:")
.with_color(color::BLACK)
.with_text_size(32)
.add_child(ui);
Slider::new(*value)
.with_size(size!(50%, 30))
.with_track_height(1.)
.with_handle_size((20., 1.))
.with_handle(NinePatchFrame::from_asset(*asset).with_color(color::CYAN))
.with_track(NinePatchFrame::from_asset(*asset))
.with_track_active(NinePatchFrame::from_asset(*asset).with_color(color::SKY_BLUE))
.on_change(SetValue)
.add_child(ui);
})
.add_root(ui, size);
ui.process_signals::<SetValue>(|signal| *value = signal.0);
}
);

View file

@ -0,0 +1,126 @@
//TODO: finish this demo
use hui::{
color, size,
draw::{ImageHandle, TextureFormat},
layout::{Alignment, Direction},
element::{
container::Container,
frame_view::FrameView,
image::Image,
text::Text,
UiElementExt
},
};
#[path = "../boilerplate.rs"]
#[macro_use]
mod boilerplate;
struct Stuff {
vscode_icon: ImageHandle,
}
ui_main!(
"hUI: vscode demo",
init: |ui| {
let handle = ui.add_font(include_bytes!("../assets/fira/FiraSans-Light.ttf"));
ui.push_font(handle);
Stuff {
vscode_icon: ui.add_image(TextureFormat::Rgba, include_bytes!("../assets/icons/visual-studio-code-icon_32x32.rgba"), 32),
}
},
run: |ui, size, stuff| {
Container::default()
.with_size(size!(100%))
.with_children(|ui| {
Container::default()
.with_size(size!(100%, auto))
.with_direction(Direction::Horizontal)
.with_align((Alignment::Begin, Alignment::Center))
.with_padding(5.)
.with_gap(15.)
.with_background(color::rgb_hex(0x3d3c3e))
.with_wrap(true) //XXX: not authentic but great for demostration
.with_children(|ui| {
Image::new(stuff.vscode_icon)
.with_size(size!(auto, 24))
.add_child(ui);
for item in ["File", "Edit", "Selection", "View", "Go", "Run", "Terminal", "Help"] {
Text::new(item)
.with_text_size(15)
.add_child(ui);
}
Container::default()
.with_size(size!(100%=, 100%))
.with_align((Alignment::End, Alignment::Center))
.with_children(|ui| {
Text::new("- ×")
.with_text_size(32)
.add_child(ui);
})
.add_child(ui);
})
.add_child(ui);
FrameView::default()
.with_size(size!(100%, 1))
.with_frame(color::rgb_hex(0x2d2d30))
.add_child(ui);
Container::default()
.with_size(size!(100%, 100%=))
.with_direction(Direction::Horizontal)
.with_children(|ui| {
// Sidebar:
Container::default()
.with_size(size!(54, 100%))
.with_background(color::rgb_hex(0x343334))
.add_child(ui);
FrameView::default()
.with_size(size!(1, 100%))
.with_frame(color::rgb_hex(0x2d2d30))
.add_child(ui);
// Explorer pane:
Container::default()
.with_size(size!(200, 100%))
.with_padding((15., 8.))
.with_background(color::rgb_hex(0x262526))
.with_children(|ui| {
Text::new("EXPLORER")
.add_child(ui);
})
.add_child(ui);
// "Code" pane
Container::default()
.with_size(size!(100%=, 100%))
.with_background(color::rgb_hex(0x1f1e1f))
.add_child(ui);
})
.add_child(ui);
//Status bar
Container::default()
.with_size(size!(100%, auto))
.with_background(color::rgb_hex(0x0079cc))
.with_direction(Direction::Horizontal)
.with_gap(5.)
.with_children(|ui| {
Container::default()
.with_background(color::rgb_hex(0x16815e))
.with_padding((10., 2.))
.with_children(|ui| {
Text::new("><")
.with_text_size(13)
.add_child(ui);
})
.add_child(ui);
Text::new("master")
.with_text_size(15)
.add_child(ui);
})
.add_child(ui);
})
.add_root(ui, size);
}
);

View file

@ -1,9 +1,11 @@
[package]
name = "hui-glium"
description = "Glium backend for hUI"
description = "glium render backend for `hui`"
repository = "https://github.com/griffi-gh/hui"
readme = "../README.md"
authors = ["griffi-gh <prasol258@gmail.com>"]
version = "0.0.2"
version = "0.1.0-alpha.5"
rust-version = "1.75"
edition = "2021"
license = "GPL-3.0-or-later"
publish = true
@ -14,7 +16,7 @@ include = [
]
[dependencies]
hui = { version = "^0.0", path = "../hui", default-features = false }
glium = "0.34"
glam = "0.25"
hui = { version = "=0.1.0-alpha.5", path = "../hui", default-features = false }
glium = { version = "0.34", default-features = false }
glam = "0.27"
log = "0.4"

View file

@ -1,4 +1,4 @@
#version 300 es
#version 150 core
precision highp float;
precision highp sampler2D;
@ -6,7 +6,8 @@ precision highp sampler2D;
out vec4 out_color;
in vec4 vtx_color;
in vec2 vtx_uv;
uniform sampler2D tex;
void main() {
out_color = vtx_color;
out_color = texture(tex, vtx_uv) * vtx_color;
}

View file

@ -0,0 +1,17 @@
#version 150 core
precision highp float;
uniform vec2 resolution;
in vec2 uv;
in vec4 color;
in vec2 position;
out vec4 vtx_color;
out vec2 vtx_uv;
void main() {
vtx_color = color;
vtx_uv = uv;
vec2 pos2d = (vec2(2., -2.) * (position / resolution)) + vec2(-1, 1);
gl_Position = vec4(pos2d, 0., 1.);
}

View file

@ -1,23 +1,17 @@
use std::rc::Rc;
use glam::Vec2;
use glium::{
Surface, DrawParameters, Blend,
Program, VertexBuffer, IndexBuffer,
backend::{Facade, Context},
texture::{SrgbTexture2d, RawImage2d},
index::PrimitiveType,
implement_vertex,
uniform, uniforms::{Sampler, SamplerBehavior, SamplerWrapFunction},
backend::{Context, Facade}, implement_vertex, index::PrimitiveType, texture::{RawImage2d, Texture2d}, uniform, uniforms::{MagnifySamplerFilter, MinifySamplerFilter, Sampler, SamplerBehavior, SamplerWrapFunction}, Api, Blend, DrawParameters, IndexBuffer, Program, Surface, VertexBuffer
};
use hui::{
UiInstance,
draw::{UiDrawPlan, UiVertex, BindTexture},
text::FontTextureInfo, IfModified,
draw::{TextureAtlasMeta, UiDrawCall, UiVertex}, UiInstance
};
const VERTEX_SHADER: &str = include_str!("../shaders/vertex.vert");
const FRAGMENT_SHADER: &str = include_str!("../shaders/fragment.frag");
const FRAGMENT_SHADER_TEX: &str = include_str!("../shaders/fragment_tex.frag");
const VERTEX_SHADER_GLES3: &str = include_str!("../shaders/vertex.es.vert");
const FRAGMENT_SHADER_GLES3: &str = include_str!("../shaders/fragment.es.frag");
const VERTEX_SHADER_150: &str = include_str!("../shaders/vertex.150.vert");
const FRAGMENT_SHADER_150: &str = include_str!("../shaders/fragment.150.frag");
#[derive(Clone, Copy)]
#[repr(C)]
@ -48,7 +42,7 @@ struct BufferPair {
impl BufferPair {
pub fn new<F: Facade>(facade: &F) -> Self {
log::debug!("init ui buffers...");
log::debug!("init ui buffers (empty)...");
Self {
vertex_buffer: VertexBuffer::empty_dynamic(facade, 1024).unwrap(),
index_buffer: IndexBuffer::empty_dynamic(facade, PrimitiveType::TrianglesList, 1024).unwrap(),
@ -57,6 +51,16 @@ impl BufferPair {
}
}
pub fn new_with_data<F: Facade>(facade: &F, vtx: &[Vertex], idx: &[u32]) -> Self {
log::debug!("init ui buffers (data)...");
Self {
vertex_buffer: VertexBuffer::dynamic(facade, vtx).unwrap(),
index_buffer: IndexBuffer::dynamic(facade, PrimitiveType::TrianglesList, idx).unwrap(),
vertex_count: vtx.len(),
index_count: idx.len(),
}
}
pub fn ensure_buffer_size(&mut self, need_vtx: usize, need_idx: usize) {
let current_vtx_size = self.vertex_buffer.get_size() / std::mem::size_of::<Vertex>();
let current_idx_size = self.index_buffer.get_size() / std::mem::size_of::<u32>();
@ -88,10 +92,9 @@ impl BufferPair {
self.vertex_count = vtx.len();
self.index_count = idx.len();
self.vertex_buffer.invalidate();
self.index_buffer.invalidate();
if self.vertex_count == 0 || self.index_count == 0 {
self.vertex_buffer.invalidate();
self.index_buffer.invalidate();
return
}
@ -106,78 +109,55 @@ impl BufferPair {
}
}
struct GlDrawCall {
active: bool,
buffer: BufferPair,
bind_texture: Option<Rc<SrgbTexture2d>>,
}
pub struct GliumUiRenderer {
context: Rc<Context>,
program: glium::Program,
program_tex: glium::Program,
font_texture: Option<Rc<SrgbTexture2d>>,
plan: Vec<GlDrawCall>,
ui_texture: Option<Texture2d>,
buffer_pair: Option<BufferPair>,
}
impl GliumUiRenderer {
pub fn new<F: Facade>(facade: &F) -> Self {
log::info!("initializing hui glium backend");
log::info!("initializing hui-glium");
Self {
program: Program::from_source(facade, VERTEX_SHADER, FRAGMENT_SHADER, None).unwrap(),
program_tex: Program::from_source(facade, VERTEX_SHADER, FRAGMENT_SHADER_TEX, None).unwrap(),
program: match facade.get_context().get_supported_glsl_version().0 {
Api::Gl => Program::from_source(facade, VERTEX_SHADER_150, FRAGMENT_SHADER_150, None).unwrap(),
Api::GlEs => Program::from_source(facade, VERTEX_SHADER_GLES3, FRAGMENT_SHADER_GLES3, None).unwrap(),
},
context: Rc::clone(facade.get_context()),
font_texture: None,
plan: vec![]
ui_texture: None,
buffer_pair: None,
}
}
pub fn update_draw_plan(&mut self, plan: &UiDrawPlan) {
if plan.calls.len() > self.plan.len() {
self.plan.resize_with(plan.calls.len(), || {
GlDrawCall {
buffer: BufferPair::new(&self.context),
bind_texture: None,
active: false,
}
});
} else {
for step in &mut self.plan[plan.calls.len()..] {
step.active = false;
}
}
for (idx, call) in plan.calls.iter().enumerate() {
let data_vtx = &call.vertices.iter().copied().map(Vertex::from).collect::<Vec<_>>()[..];
let data_idx = &call.indices[..];
self.plan[idx].active = true;
self.plan[idx].buffer.write_data(data_vtx, data_idx);
self.plan[idx].bind_texture = match call.bind_texture {
Some(BindTexture::FontTexture) => {
const NO_FNT_TEX: &str = "Font texture exists in draw plan but not yet inited. Make sure to call update_font_texture() *before* update_draw_plan()";
Some(Rc::clone(self.font_texture.as_ref().expect(NO_FNT_TEX)))
},
None => None,
}
fn update_buffers(&mut self, call: &UiDrawCall) {
log::trace!("updating ui buffers (tris: {})", call.indices.len() / 3);
let data_vtx = &call.vertices.iter().copied().map(Vertex::from).collect::<Vec<_>>()[..];
let data_idx = &call.indices[..];
if let Some(buffer) = &mut self.buffer_pair {
buffer.write_data(data_vtx, data_idx);
} else if !call.indices.is_empty() {
self.buffer_pair = Some(BufferPair::new_with_data(&self.context, data_vtx, data_idx));
}
}
pub fn update_font_texture(&mut self, font_texture: &FontTextureInfo) {
log::debug!("updating font texture");
self.font_texture = Some(Rc::new(SrgbTexture2d::new(
fn update_texture_atlas(&mut self, atlas: &TextureAtlasMeta) {
log::trace!("updating ui atlas texture");
self.ui_texture = Some(Texture2d::new(
&self.context,
RawImage2d::from_raw_rgba(
font_texture.data.to_owned(),
(font_texture.size.x, font_texture.size.y)
atlas.data.to_owned(),
(atlas.size.x, atlas.size.y)
)
).unwrap()));
).unwrap());
}
pub fn update(&mut self, hui: &UiInstance) {
if let Some(texture) = hui.font_texture().if_modified() {
self.update_font_texture(texture);
pub fn update(&mut self, instance: &UiInstance) {
if self.ui_texture.is_none() || instance.atlas().modified {
self.update_texture_atlas(&instance.atlas());
}
if let Some(plan) = hui.draw_plan().if_modified() {
self.update_draw_plan(plan);
if self.buffer_pair.is_none() || instance.draw_call().0 {
self.update_buffers(instance.draw_call().1);
}
}
@ -187,43 +167,30 @@ impl GliumUiRenderer {
..Default::default()
};
for step in &self.plan {
if !step.active {
continue
if let Some(buffer) = &self.buffer_pair {
if buffer.is_empty() {
return
}
if step.buffer.is_empty() {
continue
}
let vtx_buffer = buffer.vertex_buffer.slice(0..buffer.vertex_count).unwrap();
let idx_buffer = buffer.index_buffer.slice(0..buffer.index_count).unwrap();
let vtx_buffer = step.buffer.vertex_buffer.slice(0..step.buffer.vertex_count).unwrap();
let idx_buffer = step.buffer.index_buffer.slice(0..step.buffer.index_count).unwrap();
if let Some(bind_texture) = step.bind_texture.as_ref() {
frame.draw(
vtx_buffer,
idx_buffer,
&self.program_tex,
&uniform! {
resolution: resolution.to_array(),
tex: Sampler(bind_texture.as_ref(), SamplerBehavior {
wrap_function: (SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp),
..Default::default()
}),
},
&params,
).unwrap();
} else {
frame.draw(
vtx_buffer,
idx_buffer,
&self.program,
&uniform! {
resolution: resolution.to_array(),
},
&params,
).unwrap();
}
frame.draw(
vtx_buffer,
idx_buffer,
&self.program,
&uniform! {
resolution: resolution.to_array(),
tex: Sampler(self.ui_texture.as_ref().unwrap(), SamplerBehavior {
max_anisotropy: 1,
magnify_filter: MagnifySamplerFilter::Nearest,
minify_filter: MinifySamplerFilter::Linear,
wrap_function: (SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp),
..Default::default()
}),
},
&params,
).unwrap();
}
}
}

23
hui-wgpu/Cargo.toml Normal file
View file

@ -0,0 +1,23 @@
[package]
name = "hui-wgpu"
description = "wgpu render backend for `hui`"
repository = "https://github.com/griffi-gh/hui"
readme = "../README.md"
authors = ["griffi-gh <prasol258@gmail.com>"]
version = "0.1.0-alpha.5"
rust-version = "1.75"
edition = "2021"
license = "GPL-3.0-or-later"
publish = true
include = [
"shaders/**/*",
"src/**/*.rs",
"Cargo.toml",
]
[dependencies]
hui = { version = "=0.1.0-alpha.5", path = "../hui", default-features = false }
wgpu = { version = "0.20", default-features = false, features = ["wgsl"]}
bytemuck = "1.15"
log = "0.4"
glam = "0.27"

35
hui-wgpu/shaders/ui.wgsl Normal file
View file

@ -0,0 +1,35 @@
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>,
@location(2) color: vec4<f32>,
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) color: vec4<f32>,
};
@vertex
fn vs_main(
in: VertexInput,
) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4<f32>(in.position, 1.0);
out.uv = in.uv;
out.color = in.color;
return out;
}
@group(0) @binding(0)
var t_diffuse: texture_2d<f32>;
@group(0) @binding(1)
var s_diffuse: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
//HACK: This is a hack to convert the color to sRGB
let srgb_color = pow(in.color, vec4<f32>(2.2, 2.2, 2.2, 1.0));
return textureSample(t_diffuse, s_diffuse, in.uv) * srgb_color;
}

321
hui-wgpu/src/lib.rs Normal file
View file

@ -0,0 +1,321 @@
use glam::{vec2, Vec2};
use hui::{draw::{TextureAtlasMeta, UiDrawCall, UiVertex}, UiInstance};
const DEFAULT_BUFFER_SIZE: u64 = 1024;
const DEFAULT_TEXTURE_SIZE: u32 = 512;
const SHADER_MODULE: &str = include_str!("../shaders/ui.wgsl");
#[derive(Clone, Copy)]
#[repr(C, packed)]
struct WgpuVertex {
position: [f32; 2],
uv: [f32; 2],
color: [f32; 4],
}
unsafe impl bytemuck::Pod for WgpuVertex {}
unsafe impl bytemuck::Zeroable for WgpuVertex {}
impl WgpuVertex {
pub const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<WgpuVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![
0 => Float32x2,
1 => Float32x2,
2 => Float32x4,
],
};
}
impl From<UiVertex> for WgpuVertex {
fn from(v: UiVertex) -> Self {
Self {
position: v.position.to_array(),
uv: v.uv.to_array(),
color: v.color.to_array(),
}
}
}
pub struct WgpuUiRenderer {
pub modified: bool,
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub vertex_count: usize,
pub index_count: usize,
pub bind_group_layout: wgpu::BindGroupLayout,
pub bind_group: wgpu::BindGroup,
pub pipeline: wgpu::RenderPipeline,
pub texture: wgpu::Texture,
pub texture_view: wgpu::TextureView,
pub texture_sampler: wgpu::Sampler,
}
impl WgpuUiRenderer {
pub fn new(
device: &wgpu::Device,
surface_format: wgpu::TextureFormat,
) -> Self {
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("ui_vertex_buffer"),
size: std::mem::size_of::<WgpuVertex>() as u64 * DEFAULT_BUFFER_SIZE,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("hui_index_buffer"),
size: std::mem::size_of::<u32>() as u64 * DEFAULT_BUFFER_SIZE,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("ui_texture"),
size: wgpu::Extent3d {
width: DEFAULT_TEXTURE_SIZE,
height: DEFAULT_TEXTURE_SIZE,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("ui_bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
},
],
});
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("ui_texture_view"),
..Default::default()
});
let texture_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("ui_texture_sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("ui_bind_group"),
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&texture_sampler),
},
],
});
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("ui_vertex_shader"),
source: wgpu::ShaderSource::Wgsl(SHADER_MODULE.into()),
});
let pipeline = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("ui_pipeline_layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("ui_pipeline"),
layout: Some(&pipeline),
vertex: wgpu::VertexState {
module: &shader_module,
compilation_options: wgpu::PipelineCompilationOptions::default(),
entry_point: "vs_main",
buffers: &[WgpuVertex::LAYOUT],
},
fragment: Some(wgpu::FragmentState {
module: &shader_module,
compilation_options: wgpu::PipelineCompilationOptions::default(),
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::COLOR,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
unclipped_depth: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
});
Self {
modified: true,
vertex_buffer,
index_buffer,
vertex_count: 0,
index_count: 0,
bind_group_layout,
bind_group,
texture,
texture_view,
texture_sampler,
pipeline,
}
}
fn update_buffers(&mut self, call: &UiDrawCall, queue: &wgpu::Queue, device: &wgpu::Device, resolution: Vec2) {
let data_vtx = call.vertices.iter()
.copied()
.map(|x| {
let mut v = x;
v.position = vec2(1., -1.) * ((v.position / resolution) * 2.0 - 1.0);
v
})
.map(WgpuVertex::from)
.collect::<Vec<_>>();
let data_idx = &call.indices[..];
let data_vtx_view = bytemuck::cast_slice(&data_vtx);
let data_idx_view = bytemuck::cast_slice(data_idx);
self.vertex_count = call.vertices.len();
self.index_count = call.indices.len();
if data_vtx.is_empty() || data_idx.is_empty() {
return
}
if data_vtx_view.len() as u64 > self.vertex_buffer.size() {
self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("ui_vertex_buffer"),
size: (data_vtx_view.len() as u64).next_power_of_two(),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
}
if data_idx_view.len() as u64 > self.index_buffer.size() {
self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("ui_index_buffer"),
size: (data_idx_view.len() as u64).next_power_of_two(),
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
}
queue.write_buffer(&self.vertex_buffer, 0, data_vtx_view);
queue.write_buffer(&self.index_buffer, 0, data_idx_view);
}
fn update_texture(&self, meta: TextureAtlasMeta, queue: &wgpu::Queue) {
//TODO URGENCY:HIGH resize texture if needed
if meta.data.len() as u32 > (self.texture.size().width * self.texture.size().height * 4) {
unimplemented!("texture resize not implemented");
}
queue.write_texture(
wgpu::ImageCopyTexture {
texture: &self.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
meta.data,
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(meta.size.x * 4),
rows_per_image: Some(meta.size.y),
},
wgpu::Extent3d {
width: meta.size.x,
height: meta.size.y,
depth_or_array_layers: 1,
}
);
}
pub fn update(
&mut self,
instance: &UiInstance,
queue: &wgpu::Queue,
device: &wgpu::Device,
resolution: Vec2,
) {
let (modified, call) = instance.draw_call();
if self.modified || modified {
self.update_buffers(call, queue, device, resolution);
}
let meta = instance.atlas();
if self.modified || meta.modified {
self.update_texture(meta, queue);
}
self.modified = false;
}
pub fn draw(
&self,
encoder: &mut wgpu::CommandEncoder,
surface_view: &wgpu::TextureView,
) {
if self.vertex_count == 0 || self.index_count == 0 {
return
}
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("ui_render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: surface_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
});
let vtx_size = self.vertex_count as u64 * std::mem::size_of::<WgpuVertex>() as u64;
let idx_size = self.index_count as u64 * std::mem::size_of::<u32>() as u64;
rpass.set_pipeline(&self.pipeline);
rpass.set_bind_group(0, &self.bind_group, &[]);
rpass.set_vertex_buffer(0, self.vertex_buffer.slice(0..vtx_size));
rpass.set_index_buffer(self.index_buffer.slice(..idx_size), wgpu::IndexFormat::Uint32);
rpass.draw_indexed(0..self.index_count as u32, 0, 0..1);
}
}

26
hui-winit/Cargo.toml Normal file
View file

@ -0,0 +1,26 @@
[package]
name = "hui-winit"
description = "winit platform backend for `hui`"
repository = "https://github.com/griffi-gh/hui"
readme = "../README.md"
authors = ["griffi-gh <prasol258@gmail.com>"]
version = "0.1.0-alpha.5"
edition = "2021"
license = "GPL-3.0-or-later"
publish = true
include = [
"src/**/*.rs",
"Cargo.toml",
]
[dependencies]
hui = { version = "=0.1.0-alpha.5", path = "../hui", default-features = false }
winit_30 = { package = "winit", version = "0.30", default-features = false, optional = true }
winit_29 = { package = "winit", version = "0.29", default-features = false, optional = true }
glam = "0.27"
log = "0.4"
[features]
default = []
winit_30 = ["dep:winit_30"]
winit_29 = ["dep:winit_29"]

38
hui-winit/src/lib.rs Normal file
View file

@ -0,0 +1,38 @@
#[cfg(all(feature = "winit_30", feature = "winit_29"))]
compile_error!("Only one of the winit_30 and winit_29 features can be enabled at a time");
#[cfg(not(any(feature = "winit_30", feature = "winit_29")))]
compile_error!("One of the winit_30 and winit_29 features must be enabled");
#[cfg(feature = "winit_30")] extern crate winit_30 as winit;
#[cfg(feature = "winit_29")] extern crate winit_29 as winit;
use glam::vec2;
use hui::{event::UiEvent, UiInstance};
use winit::event::{Event, WindowEvent, MouseButton, ElementState};
//TODO: check window id
pub fn handle_winit_event<T>(ui: &mut UiInstance, event: &Event<T>) {
if let Event::WindowEvent { event, .. } = event {
match event {
WindowEvent::CursorMoved { position, .. } => {
ui.push_event(UiEvent::MouseMove(vec2(position.x as f32, position.y as f32)));
},
WindowEvent::MouseInput { state, button, .. } => {
ui.push_event(UiEvent::MouseButton {
button: match button {
MouseButton::Left => hui::input::MouseButton::Primary,
MouseButton::Right => hui::input::MouseButton::Secondary,
MouseButton::Middle => hui::input::MouseButton::Middle,
MouseButton::Other(id) => hui::input::MouseButton::Other(*id as u8),
_ => return,
},
state: match state {
ElementState::Pressed => hui::input::ButtonState::Pressed,
ElementState::Released => hui::input::ButtonState::Released,
},
})
},
//TODO: translate keyboard input
_ => (),
}
}
}

View file

@ -2,9 +2,10 @@
name = "hui"
description = "Simple UI library for games and other interactive applications"
repository = "https://github.com/griffi-gh/hui"
readme = "../README.md"
authors = ["griffi-gh <prasol258@gmail.com>"]
rust-version = "1.75"
version = "0.0.2"
version = "0.1.0-alpha.5"
edition = "2021"
license = "GPL-3.0-or-later"
publish = true
@ -15,15 +16,88 @@ include = [
]
[dependencies]
hui-derive = { version = "0.1.0-alpha.5", path = "../hui-derive", optional = true }
hashbrown = "0.14"
nohash-hasher = "0.2"
glam = "0.25"
glam = "0.27"
fontdue = "0.8"
rect_packer = "0.2"
log = "0.4"
document-features = "0.2"
derive_setters = "0.1"
derive_more = "0.99"
tinyset = "0.4"
image = { version = "0.25", default-features = false, optional = true }
rustc-hash = "1.1"
[features]
default = ["builtin_elements", "builtin_font"]
default = ["el_all", "image", "builtin_font", "pixel_perfect_text", "derive"]
## Enable derive macros
derive = ["dep:hui-derive"]
## Enable image loading support using the `image` crate
image = ["dep:image"]
## Enable the built-in font (ProggyTiny, adds *35kb* to the executable)
builtin_font = []
builtin_elements = []
#parallel = ["dep:rayon", "fontdue/parallel"]
#! #### Pixel-perfect rendering:
## Round all vertex positions to nearest integer coordinates (not recommended)
pixel_perfect = ["pixel_perfect_text"]
## Apply pixel-perfect rendering hack to text (fixes blurry text rendering)
pixel_perfect_text = []
#! Make sure to disable both features if you are not rendering UI "as-is" at 1:1 scale\
#! For exmaple, you should disable them if using DPI (or any other form of) scaling while passing the virtual resolution to the ui or rendering it in 3d space
#! #### Built-in elements:
## Enable all built-in elements
el_all = [
"el_container",
"el_frame_view",
"el_spacer",
"el_br",
"el_text",
"el_image",
"el_progress_bar",
"el_slider",
"el_transformer",
"el_interactable",
]
## Enable the built-in `Container` element
el_container = []
## Enable the built-in `FrameView` element
el_frame_view = []
## Enable the built-in `Spacer` element
el_spacer = []
## Enable the built-in `Break` element
el_br = []
## Enable the built-in `Text` element
el_text = []
## Enable the built-in `Image` element
el_image = []
## Enable the built-in `ProgressBar` element
el_progress_bar = []
## Enable the built-in `Slider` element
el_slider = []
## Enable the built-in `Transformer` element
el_transformer = []
## Enable the built-in `Interactable` element
el_interactable = []
# ## Enable multi-threading support (currently only affects some 3rd-party libraries)
# parallel = ["fontdue/parallel"]

128
hui/src/color.rs Normal file
View file

@ -0,0 +1,128 @@
//! various predefined color constants and helper functions
use glam::{vec4, Vec4};
/// Create a color from red, green, blue components
pub fn rgb(r: u8, g: u8, b: u8) -> Vec4 {
vec4(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0)
}
/// Create a color from red, green, blue, alpha components
pub fn rgba(r: u8, g: u8, b: u8, a: u8) -> Vec4 {
vec4(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0)
}
/// Create an RGB color from a u32 (/hex) value
pub fn rgb_hex(value: u32) -> Vec4 {
let r = (value >> 16) & 0xff;
let g = (value >> 8) & 0xff;
let b = value & 0xff;
vec4(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0)
}
/// Create an RGBA color from a u32 (/hex) value
pub fn rgba_hex(value: u32) -> Vec4 {
let r = (value >> 16) & 0xff;
let g = (value >> 8) & 0xff;
let b = value & 0xff;
let a = (value >> 24) & 0xff;
vec4(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0)
}
#[cfg_attr(doc, doc="<span style='display: inline-block; background: repeating-conic-gradient(grey 0 25%,darkgrey 0 50%) 50%/8px 8px; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#00000000` Transparent
pub const TRANSPARENT: Vec4 = vec4(0.0, 0.0, 0.0, 0.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #000000; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#000000` Black
pub const BLACK: Vec4 = vec4(0.0, 0.0, 0.0, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #ffffff; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#ffffff` White
pub const WHITE: Vec4 = vec4(1.0, 1.0, 1.0, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #ff0000; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#ff0000` Red
pub const RED: Vec4 = vec4(1.0, 0.0, 0.0, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #800000; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#800000` Dark red
pub const DARK_RED: Vec4 = vec4(0.5, 0.0, 0.0, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #00ff00; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#00ff00` Green
pub const GREEN: Vec4 = vec4(0.0, 1.0, 0.0, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #008000; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#008000` Dark green
pub const DARK_GREEN: Vec4 = vec4(0.0, 0.5, 0.0, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #0000ff; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#0000ff` Blue
pub const BLUE: Vec4 = vec4(0.0, 0.0, 1.0, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #000080; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#000080` Dark blue
pub const DARK_BLUE: Vec4 = vec4(0.0, 0.0, 0.5, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #ffff00; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#ffff00` Yellow
pub const YELLOW: Vec4 = vec4(1.0, 1.0, 0.0, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #00ffff; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#00ffff` Cyan
pub const CYAN: Vec4 = vec4(0.0, 1.0, 1.0, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #ff00ff; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#ff00ff` Magenta
pub const MAGENTA: Vec4 = vec4(1.0, 0.0, 1.0, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #808080; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#808080` Gray
pub const GRAY: Vec4 = vec4(0.5, 0.5, 0.5, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #c0c0c0; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#c0c0c0` Light gray
pub const LIGHT_GRAY: Vec4 = vec4(0.75, 0.75, 0.75, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #404040; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#404040` Dark gray
pub const DARK_GRAY: Vec4 = vec4(0.25, 0.25, 0.25, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #ff8000; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#ff8000` Orange
pub const ORANGE: Vec4 = vec4(1.0, 0.5, 0.0, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #804000; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#804000` Brown
pub const BROWN: Vec4 = vec4(0.5, 0.25, 0.0, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #ff80ff; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#ff80ff` Pink
pub const PINK: Vec4 = vec4(1.0, 0.5, 1.0, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #800080; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#800080` Purple
pub const PURPLE: Vec4 = vec4(0.5, 0.0, 0.5, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #80ff00; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#80ff00` Lime
pub const LIME: Vec4 = vec4(0.5, 1.0, 0.0, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #008080; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#008080` Teal
pub const TEAL: Vec4 = vec4(0.0, 0.5, 0.5, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #004080; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#004080` Indigo
pub const INDIGO: Vec4 = vec4(0.0, 0.25, 0.5, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #808000; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#808000` Olive
pub const OLIVE: Vec4 = vec4(0.5, 0.5, 0.0, 1.0);
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #87ceeb; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
/// `#87ceeb` Sky blue
pub const SKY_BLUE: Vec4 = vec4(0.53, 0.81, 0.92, 1.0);
//TODO color macro

View file

@ -1,9 +1,26 @@
use crate::{IfModified, text::{TextRenderer, FontHandle}};
//! draw commands, tesselation and UI rendering.
use crate::{
rect::Corners,
text::{FontHandle, TextRenderer}
};
pub(crate) mod atlas;
use atlas::TextureAtlasManager;
pub use atlas::{ImageHandle, TextureAtlasMeta, TextureFormat, ImageCtx};
mod corner_radius;
pub use corner_radius::RoundedCorners;
use std::borrow::Cow;
use fontdue::layout::{Layout, CoordinateSystem, TextStyle};
use glam::{Vec2, Vec4, vec2};
use glam::{vec2, Vec2, Affine2, Vec4};
//TODO: circle draw command
/// Available draw commands
/// - Rectangle: Filled, colored rectangle, with optional rounded corners and texture
/// - Text: Draw text using the specified font, size, color, and position
#[derive(Clone, Debug, PartialEq)]
pub enum UiDrawCommand {
///Filled, colored rectangle
@ -13,13 +30,20 @@ pub enum UiDrawCommand {
///Size in pixels
size: Vec2,
///Color (RGBA)
color: Vec4,
color: Corners<Vec4>,
///Texture
texture: Option<ImageHandle>,
///Sub-UV coordinates for the texture
texture_uv: Option<Corners<Vec2>>,
///Rounded corners
rounded_corners: Option<RoundedCorners>,
},
/// Draw text using the specified font, size, color, and position
Text {
///Position in pixels
position: Vec2,
///Font size
size: u8,
size: u16,
///Color (RGBA)
color: Vec4,
///Text to draw
@ -27,14 +51,21 @@ pub enum UiDrawCommand {
///Font handle to use
font: FontHandle,
},
/// Push a transformation matrix to the stack
PushTransform(Affine2),
/// Pop a transformation matrix from the stack
PopTransform,
//TODO PushClip PopClip
}
/// List of draw commands
#[derive(Default)]
pub struct UiDrawCommands {
pub struct UiDrawCommandList {
pub commands: Vec<UiDrawCommand>,
}
impl UiDrawCommands {
impl UiDrawCommandList {
/// Add a draw command to the list
pub fn add(&mut self, command: UiDrawCommand) {
self.commands.push(command);
}
@ -47,174 +78,319 @@ impl UiDrawCommands {
// }
// }
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BindTexture {
FontTexture,
//UserDefined(usize),
}
#[derive(Clone, Copy, Debug, PartialEq)]
/// A vertex for UI rendering
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct UiVertex {
pub position: Vec2,
pub color: Vec4,
pub uv: Vec2,
}
/// Represents a single draw call (vertices + indices), should be handled by the render backend
#[derive(Default)]
pub struct UiDrawCall {
pub vertices: Vec<UiVertex>,
pub indices: Vec<u32>,
pub bind_texture: Option<BindTexture>,
}
#[derive(Default)]
pub struct UiDrawPlan {
pub calls: Vec<UiDrawCall>
}
impl UiDrawCall {
/// Tesselate the UI and build a complete draw plan from a list of draw commands
pub(crate) fn build(draw_commands: &UiDrawCommandList, atlas: &mut TextureAtlasManager, text_renderer: &mut TextRenderer) -> Self {
let mut trans_stack = Vec::new();
let mut draw_call = UiDrawCall::default();
struct CallSwapper {
calls: Vec<UiDrawCall>,
call: UiDrawCall,
}
impl CallSwapper {
pub fn new() -> Self {
Self {
calls: vec![],
call: UiDrawCall::default(),
}
}
pub fn current(&self) -> &UiDrawCall {
&self.call
}
pub fn current_mut(&mut self) -> &mut UiDrawCall {
&mut self.call
}
pub fn swap(&mut self) {
self.calls.push(std::mem::replace(&mut self.call, UiDrawCall::default()));
}
pub fn finish(mut self) -> Vec<UiDrawCall> {
self.calls.push(self.call);
self.calls
}
}
impl UiDrawPlan {
pub fn build(draw_commands: &UiDrawCommands, tr: &mut TextRenderer) -> Self {
let mut swapper = CallSwapper::new();
let mut prev_command = None;
for command in &draw_commands.commands {
let do_swap = if let Some(prev_command) = prev_command {
std::mem::discriminant(prev_command) != std::mem::discriminant(command)
} else {
false
};
if do_swap {
swapper.swap();
}
if do_swap || prev_command.is_none() {
match command {
UiDrawCommand::Rectangle { .. } => (),
UiDrawCommand::Text { .. } => {
swapper.current_mut().bind_texture = Some(BindTexture::FontTexture);
}
//HACK: atlas may get resized while creating new glyphs,
//which invalidates all uvs, causing corrupted-looking texture
//so we need to pregenerate font textures before generating any vertices
//we are doing *a lot* of double work here, but it's the easiest way to avoid the issue
for comamnd in &draw_commands.commands {
if let UiDrawCommand::Text { text, font: font_handle, size, .. } = comamnd {
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.append(
&[text_renderer.internal_font(*font_handle)],
&TextStyle::new(text, *size as f32, 0)
);
let glyphs = layout.glyphs();
for layout_glyph in glyphs {
if !layout_glyph.char_data.rasterize() { continue }
text_renderer.glyph(atlas, *font_handle, layout_glyph.parent, layout_glyph.key.px as u8);
}
}
}
//note to future self:
//RESIZING OR ADDING STUFF TO ATLAS AFTER THIS POINT IS A BIG NO-NO,
//DON'T DO IT EVER AGAIN UNLESS YOU WANT TO SPEND HOURS DEBUGGING
atlas.lock_atlas = true;
for command in &draw_commands.commands {
match command {
UiDrawCommand::Rectangle { position, size, color } => {
let vidx = swapper.current().vertices.len() as u32;
swapper.current_mut().indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
swapper.current_mut().vertices.extend([
UiVertex {
position: *position,
color: *color,
uv: vec2(0.0, 0.0),
},
UiVertex {
position: *position + vec2(size.x, 0.0),
color: *color,
uv: vec2(1.0, 0.0),
},
UiVertex {
position: *position + *size,
color: *color,
uv: vec2(1.0, 1.0),
},
UiVertex {
position: *position + vec2(0.0, size.y),
color: *color,
uv: vec2(0.0, 1.0),
},
]);
UiDrawCommand::PushTransform(trans) => {
//Take note of the current index, and the transformation matrix\
//We will actually apply the transformation matrix when we pop it,
//to all vertices between the current index and the index we pushed
trans_stack.push((trans, draw_call.vertices.len() as u32));
},
UiDrawCommand::Text { position, size, color, text, font } => {
UiDrawCommand::PopTransform => {
//Pop the transformation matrix and apply it to all vertices between the current index and the index we pushed
let (&trans, idx) = trans_stack.pop().expect("Unbalanced push/pop transform");
//If Push is immediately followed by a pop (which is dumb but possible), we don't need to do anything
//(this can also happen if push and pop are separated by a draw command that doesn't add any vertices, like a text command with an empty string)
if idx == draw_call.vertices.len() as u32 {
continue
}
//Kinda a hack:
//We want to apply the transform aronnd the center, so we need to compute the center of the vertices
//We won't actually do that, we will compute the center of the bounding box of the vertices
let mut min = Vec2::splat(std::f32::INFINITY);
let mut max = Vec2::splat(std::f32::NEG_INFINITY);
for v in &draw_call.vertices[idx as usize..] {
min = min.min(v.position);
max = max.max(v.position);
}
//TODO: make the point of transform configurable
let center = (min + max) / 2.;
//Apply trans mtx to all vertices between idx and the current index
for v in &mut draw_call.vertices[idx as usize..] {
v.position -= center;
v.position = trans.transform_point2(v.position);
v.position += center;
}
},
UiDrawCommand::Rectangle { position, size, color, texture, texture_uv, rounded_corners } => {
let uvs = texture
.map(|x| atlas.get_uv(x))
.flatten()
.map(|guv| {
if let Some(texture_uv) = texture_uv {
//XXX: this may not work if the texture is rotated
//also is this slow?
let top = guv.top_left.lerp(guv.top_right, texture_uv.top_left.x);
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.top_left.x);
let top_left = top.lerp(bottom, texture_uv.top_left.y);
let top = guv.top_left.lerp(guv.top_right, texture_uv.top_right.x);
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.top_right.x);
let top_right = top.lerp(bottom, texture_uv.top_right.y);
let top = guv.top_left.lerp(guv.top_right, texture_uv.bottom_left.x);
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.bottom_left.x);
let bottom_left = top.lerp(bottom, texture_uv.bottom_left.y);
let top = guv.top_left.lerp(guv.top_right, texture_uv.bottom_right.x);
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.bottom_right.x);
let bottom_right = top.lerp(bottom, texture_uv.bottom_right.y);
Corners { top_left, top_right, bottom_left, bottom_right }
} else {
guv
}
})
.unwrap_or(Corners::all(Vec2::ZERO));
let vidx = draw_call.vertices.len() as u32;
if let Some(corner) = rounded_corners.filter(|x| x.radius.max_f32() > 0.0) {
//this code is stupid as fuck
//but it works... i think?
//maybe some verts end up missing, but it's close enough...
//Random vert in the center for no reason
//lol
draw_call.vertices.push(UiVertex {
position: *position + *size * vec2(0.5, 0.5),
color: (color.bottom_left + color.bottom_right + color.top_left + color.top_right) / 4.,
//TODO: fix this uv
uv: vec2(0., 0.),
});
//TODO: fix some corners tris being invisible (but it's already close enough lol)
let rounded_corner_verts = corner.point_count.get() as u32;
for i in 0..rounded_corner_verts {
let cratio = i as f32 / rounded_corner_verts as f32;
let angle = cratio * std::f32::consts::PI * 0.5;
let x = angle.sin();
let y = angle.cos();
let mut corner_impl = |rp: Vec2, color: &Corners<Vec4>| {
let rrp = rp / *size;
let color_at_point =
color.bottom_right * rrp.x * rrp.y +
color.top_right * rrp.x * (1. - rrp.y) +
color.bottom_left * (1. - rrp.x) * rrp.y +
color.top_left * (1. - rrp.x) * (1. - rrp.y);
let uv_at_point =
uvs.bottom_right * rrp.x * rrp.y +
uvs.top_right * rrp.x * (1. - rrp.y) +
uvs.bottom_left * (1. - rrp.x) * rrp.y +
uvs.top_left * (1. - rrp.x) * (1. - rrp.y);
draw_call.vertices.push(UiVertex {
position: *position + rp,
color: color_at_point,
uv: uv_at_point,
});
};
//Top-right corner
corner_impl(
vec2(x, 1. - y) * corner.radius.top_right + vec2(size.x - corner.radius.top_right, 0.),
color,
);
//Bottom-right corner
corner_impl(
vec2(x - 1., y) * corner.radius.bottom_right + vec2(size.x, size.y - corner.radius.bottom_right),
color,
);
//Bottom-left corner
corner_impl(
vec2(1. - x, y) * corner.radius.bottom_left + vec2(0., size.y - corner.radius.bottom_left),
color,
);
//Top-left corner
corner_impl(
vec2(1. - x, 1. - y) * corner.radius.top_left,
color,
);
// mental illness:
if i > 0 {
draw_call.indices.extend([
//Top-right corner
vidx,
vidx + 1 + (i - 1) * 4,
vidx + 1 + i * 4,
//Bottom-right corner
vidx,
vidx + 1 + (i - 1) * 4 + 1,
vidx + 1 + i * 4 + 1,
//Bottom-left corner
vidx,
vidx + 1 + (i - 1) * 4 + 2,
vidx + 1 + i * 4 + 2,
//Top-left corner
vidx,
vidx + 1 + (i - 1) * 4 + 3,
vidx + 1 + i * 4 + 3,
]);
}
}
//Fill in the rest
//mental illness 2:
draw_call.indices.extend([
//Top
vidx,
vidx + 4,
vidx + 1,
//Right?, i think
vidx,
vidx + 1 + (rounded_corner_verts - 1) * 4,
vidx + 1 + (rounded_corner_verts - 1) * 4 + 1,
//Left???
vidx,
vidx + 1 + (rounded_corner_verts - 1) * 4 + 2,
vidx + 1 + (rounded_corner_verts - 1) * 4 + 3,
//Bottom???
vidx,
vidx + 3,
vidx + 2,
]);
} else {
//...Normal rectangle
draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
draw_call.vertices.extend([
UiVertex {
position: *position,
color: color.top_left,
uv: uvs.top_left,
},
UiVertex {
position: *position + vec2(size.x, 0.0),
color: color.top_right,
uv: uvs.top_right,
},
UiVertex {
position: *position + *size,
color: color.bottom_right,
uv: uvs.bottom_right,
},
UiVertex {
position: *position + vec2(0.0, size.y),
color: color.bottom_left,
uv: uvs.bottom_left,
},
]);
}
},
UiDrawCommand::Text { position, size, color, text, font: font_handle } => {
if text.is_empty() {
continue
}
//XXX: should we be doing this every time?
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.append(
&[tr.internal_font(*font)],
&[text_renderer.internal_font(*font_handle)],
&TextStyle::new(text, *size as f32, 0)
);
let glyphs = layout.glyphs();
//let mut rpos_x = 0.;
for layout_glyph in glyphs {
if !layout_glyph.char_data.rasterize() {
continue
}
let vidx = swapper.current().vertices.len() as u32;
let glyph = tr.glyph(*font, layout_glyph.parent, layout_glyph.key.px as u8);
//rpos_x += glyph.metrics.advance_width;//glyph.metrics.advance_width;
swapper.current_mut().indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
let p0x = glyph.position.x as f32 / 1024.;
let p1x = (glyph.position.x + glyph.size.x as i32) as f32 / 1024.;
let p0y = glyph.position.y as f32 / 1024.;
let p1y = (glyph.position.y + glyph.size.y as i32) as f32 / 1024.;
swapper.current_mut().vertices.extend([
let vidx = draw_call.vertices.len() as u32;
let glyph = text_renderer.glyph(atlas, *font_handle, layout_glyph.parent, layout_glyph.key.px as u8);
let uv = atlas.get_uv(glyph.texture).unwrap();
draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
draw_call.vertices.extend([
UiVertex {
position: *position + vec2(layout_glyph.x, layout_glyph.y),
color: *color,
uv: vec2(p0x, p0y),
uv: uv.top_left,
},
UiVertex {
position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y),
color: *color,
uv: vec2(p1x, p0y),
uv: uv.top_right,
},
UiVertex {
position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y + glyph.metrics.height as f32),
color: *color,
uv: vec2(p1x, p1y),
uv: uv.bottom_right,
},
UiVertex {
position: *position + vec2(layout_glyph.x, layout_glyph.y + glyph.metrics.height as f32),
color: *color,
uv: vec2(p0x, p1y),
uv: uv.bottom_left,
},
]);
#[cfg(all(
feature = "pixel_perfect_text",
not(feature = "pixel_perfect")
))] {
//Round the position of the vertices to the nearest pixel, unless any transformations are active
if trans_stack.is_empty() {
for vtx in &mut draw_call.vertices[(vidx as usize)..] {
vtx.position = vtx.position.round()
}
}
}
}
}
}
prev_command = Some(command);
}
Self {
calls: swapper.finish()
}
}
}
impl IfModified<UiDrawPlan> for (bool, &UiDrawPlan) {
fn if_modified(&self) -> Option<&UiDrawPlan> {
match self.0 {
true => Some(self.1),
false => None,
}
atlas.lock_atlas = false;
#[cfg(feature = "pixel_perfect")]
draw_call.vertices.iter_mut().for_each(|v| {
v.position = v.position.round()
});
draw_call
}
}

297
hui/src/draw/atlas.rs Normal file
View file

@ -0,0 +1,297 @@
use glam::{uvec2, vec2, UVec2, Vec2};
use hashbrown::HashMap;
use nohash_hasher::BuildNoHashHasher;
use rect_packer::DensePacker;
use crate::rect::Corners;
const RGBA_CHANNEL_COUNT: u32 = 4;
//TODO make this work
const ALLOW_ROTATION: bool = false;
/// Texture format of the source texture data
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub enum TextureFormat {
/// The data is stored in RGBA format, with 1 byte (8 bits) per channel
#[default]
Rgba,
/// The data is copied into the Alpha channel, with 1 byte (8 bits) per channel\
/// Remaining channels are set to 255 (which can be easily shaded to any color)
///
/// This format is useful for storing grayscale textures such as icons\
/// (Please note that the internal representation is still RGBA, this is just a convenience feature)
Grayscale,
}
/// Contains a reference to the texture data, and metadata associated with it
pub struct TextureAtlasMeta<'a> {
/// Texture data\
/// The data is stored in RGBA format, with 1 byte (8 bits) per channel
pub data: &'a [u8],
/// Current size of the texture atlas\
/// Please note that this value might change
pub size: UVec2,
/// True if the atlas has been modified since the beginning of the current frame\
/// If this function returns true, the texture atlas should be re-uploaded to the GPU before rendering\
pub modified: bool,
}
/// Texture handle, stores the internal index of a texture within the texture atlas and can be cheaply copied.
///
/// Please note that dropping a handle does not deallocate the texture from the atlas, you must do it manually.
///
/// Only valid for the `UiInstance` that created it.\
/// Using it with other instances may result in panics or unexpected behavior.
///
/// Handle values are not guaranteed to be valid.\
/// Creating or transmuting an invalid handle is allowed and is *not* UB.
///
/// Internal value is an implementation detail and should not be relied upon.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct ImageHandle {
pub(crate) index: u32,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct TextureAllocation {
/// Position in the texture atlas\
/// (This is an implementation detail and should not be exposed to the user)
pub(crate) position: UVec2,
/// Requested texture size
pub size: UVec2,
/// True if the texture was rotated by 90 degrees\
/// (This is an implementation detail and should not be exposed to the user)
pub(crate) rotated: bool,
}
/// Manages a texture atlas and the allocation of space within it\
/// The atlas is alllowed to grow and resize dynamically, as needed
pub(crate) struct TextureAtlasManager {
packer: DensePacker,
count: u32,
size: UVec2,
data: Vec<u8>,
allocations: HashMap<u32, TextureAllocation, BuildNoHashHasher<u32>>,
/// Items that have been removed from the allocation list, but still affect
remove_queue: Vec<TextureAllocation>,
/// True if the atlas has been modified in a way which requires a texture reupload
/// since the beginning of the current frame
modified: bool,
/// If true, attempting to modify the atlas in a way which invalidates UVs will cause a panic\
/// Used internally to ensure that the UVs do not become invalidated mid-render
pub(crate) lock_atlas: bool,
}
impl TextureAtlasManager {
/// Create a new texture atlas with the specified size\
/// 512x512 is a good default size for most applications, and the texture atlas can grow dynamically as needed
pub fn new(size: UVec2) -> Self {
Self {
packer: DensePacker::new(size.x as i32, size.y as i32),
count: 0,
size,
data: vec![0; (size.x * size.y * RGBA_CHANNEL_COUNT) as usize],
allocations: HashMap::default(),
remove_queue: Vec::new(),
modified: true,
lock_atlas: false,
}
}
/// Resize the texture atlas to the new size in-place, preserving the existing data
pub fn resize(&mut self, new_size: UVec2) {
if self.lock_atlas {
panic!("Attempted to resize the texture atlas while the atlas is locked");
}
log::trace!("resizing texture atlas to {:?}", new_size);
if self.size == new_size {
log::warn!("Texture atlas is already the requested size");
return
}
if new_size.x > self.size.x && new_size.y > self.size.y {
self.packer.resize(new_size.x as i32, new_size.y as i32);
//Resize the data array in-place
self.data.resize((new_size.x * new_size.y * RGBA_CHANNEL_COUNT) as usize, 0);
for y in (0..self.size.y).rev() {
for x in (1..self.size.x).rev() {
let idx = ((y * self.size.x + x) * RGBA_CHANNEL_COUNT) as usize;
let new_idx = ((y * new_size.x + x) * RGBA_CHANNEL_COUNT) as usize;
for c in 0..(RGBA_CHANNEL_COUNT as usize) {
self.data[new_idx + c] = self.data[idx + c];
}
}
}
} else {
//If scaling down, just recreate the atlas from scratch (since we need to re-pack everything anyway)
todo!("Atlas downscaling is not implemented yet");
}
self.size = new_size;
self.modified = true;
}
/// Ensure that a texture with specified size would fit without resizing on the next allocation attempt\
pub fn ensure_fits(&mut self, size: UVec2) {
// Plan A: try if any of the existing items in the remove queue would fit the texture
// Plan B: purge the remove queue, recreate the packer and try again (might be expensive...!)
// TODO: implement these
// Plan C: resize the atlas
let mut new_size = self.size;
while !self.packer.can_pack(size.x as i32, size.y as i32, ALLOW_ROTATION) {
new_size *= 2;
self.packer.resize(new_size.x as i32, new_size.y as i32);
}
if new_size != self.size {
self.resize(new_size);
}
}
/// Allocate a new texture region in the atlas and return a handle to it\
/// Returns None if the texture could not be allocated due to lack of space\
/// Use `allocate` to allocate a texture and resize the atlas if necessary\
/// Does not modify the texture data
fn try_allocate(&mut self, size: UVec2) -> Option<ImageHandle> {
log::trace!("Allocating texture of size {:?}", size);
let result = self.packer.pack(size.x as i32, size.y as i32, ALLOW_ROTATION)?;
let index = self.count;
self.count += 1;
let allocation = TextureAllocation {
position: UVec2::new(result.x as u32, result.y as u32),
size,
//If the size does not match the requested size, the texture was rotated
rotated: ALLOW_ROTATION && (result.width != size.x as i32),
};
self.allocations.insert_unique_unchecked(index, allocation);
Some(ImageHandle { index })
}
/// Allocate a new texture region in the atlas and resize the atlas if necessary\
/// This function should never fail under normal circumstances.\
/// May modify the texture data if the atlas is resized
pub fn allocate(&mut self, size: UVec2) -> ImageHandle {
self.ensure_fits(size);
self.try_allocate(size).unwrap()
}
/// Allocate a new texture region in the atlas and copy the data into it\
/// This function may resize the atlas as needed, and should never fail under normal circumstances.
pub(crate) fn add_rgba(&mut self, width: usize, data: &[u8]) -> ImageHandle {
let size = uvec2(width as u32, (data.len() / (width * RGBA_CHANNEL_COUNT as usize)) as u32);
let handle: ImageHandle = self.allocate(size);
let allocation = self.allocations.get(&handle.index).unwrap();
assert!(!allocation.rotated, "Rotated textures are not implemented yet");
for y in 0..size.y {
for x in 0..size.x {
let src_idx = (y * size.x + x) * RGBA_CHANNEL_COUNT;
let dst_idx = ((allocation.position.y + y) * self.size.x + allocation.position.x + x) * RGBA_CHANNEL_COUNT;
for c in 0..RGBA_CHANNEL_COUNT as usize {
self.data[dst_idx as usize + c] = data[src_idx as usize + c];
}
}
}
self.modified = true;
handle
}
/// Works the same way as [`TextureAtlasManager::add`], but the input data is assumed to be grayscale (1 channel per pixel)\
/// The data is copied into the alpha channel of the texture, while all the other channels are set to 255\
/// May resize the atlas as needed, and should never fail under normal circumstances.
pub(crate) fn add_grayscale(&mut self, width: usize, data: &[u8]) -> ImageHandle {
let size = uvec2(width as u32, (data.len() / width) as u32);
let handle = self.allocate(size);
let allocation = self.allocations.get(&handle.index).unwrap();
assert!(!allocation.rotated, "Rotated textures are not implemented yet");
for y in 0..size.y {
for x in 0..size.x {
let src_idx = (y * size.x + x) as usize;
let dst_idx = (((allocation.position.y + y) * self.size.x + allocation.position.x + x) * RGBA_CHANNEL_COUNT) as usize;
self.data[dst_idx..(dst_idx + RGBA_CHANNEL_COUNT as usize)].copy_from_slice(&[255, 255, 255, data[src_idx]]);
}
}
self.modified = true;
handle
}
pub fn add(&mut self, width: usize, data: &[u8], format: TextureFormat) -> ImageHandle {
match format {
TextureFormat::Rgba => self.add_rgba(width, data),
TextureFormat::Grayscale => self.add_grayscale(width, data),
}
}
pub(crate) fn add_dummy(&mut self) {
let handle = self.allocate((1, 1).into());
assert!(handle.index == 0, "Dummy texture handle is not 0");
assert!(self.get(handle).unwrap().position == (0, 0).into(), "Dummy texture position is not (0, 0)");
self.data[0..4].copy_from_slice(&[255, 255, 255, 255]);
self.modified = true;
}
pub fn modify(&mut self, handle: ImageHandle) {
todo!()
}
pub fn remove(&mut self, handle: ImageHandle) {
todo!()
}
pub fn get(&self, handle: ImageHandle) -> Option<&TextureAllocation> {
self.allocations.get(&handle.index)
}
pub(crate) fn get_uv(&self, handle: ImageHandle) -> Option<Corners<Vec2>> {
let info = self.get(handle)?;
let atlas_size = self.meta().size.as_vec2();
let p0x = info.position.x as f32 / atlas_size.x;
let p1x = (info.position.x as f32 + info.size.x as f32) / atlas_size.x;
let p0y = info.position.y as f32 / atlas_size.y;
let p1y = (info.position.y as f32 + info.size.y as f32) / atlas_size.y;
Some(Corners {
top_left: vec2(p0x, p0y),
top_right: vec2(p1x, p0y),
bottom_left: vec2(p0x, p1y),
bottom_right: vec2(p1x, p1y),
})
}
/// Reset the `is_modified` flag
pub(crate) fn reset_modified(&mut self) {
self.modified = false;
}
pub fn meta(&self) -> TextureAtlasMeta {
TextureAtlasMeta {
data: &self.data,
size: self.size,
modified: self.modified,
}
}
pub fn context(&self) -> ImageCtx {
ImageCtx { atlas: self }
}
}
impl Default for TextureAtlasManager {
/// Create a new texture atlas with a default size of 512x512
fn default() -> Self {
Self::new(UVec2::new(512, 512))
}
}
/// Context that allows read-only accss to image metadata
#[derive(Clone, Copy)]
pub struct ImageCtx<'a> {
pub(crate) atlas: &'a TextureAtlasManager,
}
impl ImageCtx<'_> {
/// Get size of the image with the specified handle
///
/// Returns None if the handle is invalid for the current context
pub fn get_size(&self, handle: ImageHandle) -> Option<UVec2> {
self.atlas.get(handle).map(|a| a.size)
}
}

View file

@ -0,0 +1,55 @@
use std::num::NonZeroU16;
use crate::rect::Corners;
//TODO uneven corners (separate width/height for each corner)
/// Calculate the number of points based on the maximum corner radius
fn point_count(corners: Corners<f32>) -> NonZeroU16 {
//Increase for higher quality
const VTX_PER_CORER_RADIUS_PIXEL: f32 = 0.5;
NonZeroU16::new(
(corners.max_f32() * VTX_PER_CORER_RADIUS_PIXEL).round() as u16 + 2
).unwrap()
}
/// Low-level options for rendering rounded corners
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RoundedCorners {
/// Corner radius of each corner
pub radius: Corners<f32>,
/// Number of points to use for each corner
///
/// This value affects all corners, regardless of their individual radius
pub point_count: NonZeroU16,
}
impl From<Corners<f32>> for RoundedCorners {
/// Create a new `RoundedCorners` from [`Corners<f32>`](crate::rect::Corners)
///
/// Point count will be calculated automatically based on the maximum radius
fn from(radius: Corners<f32>) -> Self {
Self::from_radius(radius)
}
}
impl RoundedCorners {
/// Create a new `RoundedCorners` from [`Corners<f32>`](crate::rect::Corners)
///
/// Point count will be calculated automatically based on the maximum radius
pub fn from_radius(radius: Corners<f32>) -> Self {
Self {
radius,
point_count: point_count(radius),
}
}
}
impl Default for RoundedCorners {
fn default() -> Self {
Self {
radius: Corners::default(),
point_count: NonZeroU16::new(8).unwrap(),
}
}
}

View file

@ -1,29 +1,101 @@
use std::any::Any;
//! element API and built-in elements like `Container`, `Button`, `Text`, etc.
use crate::{
LayoutInfo,
draw::UiDrawCommands,
draw::{atlas::ImageCtx, UiDrawCommandList},
input::InputCtx,
layout::{LayoutInfo, Size2d},
measure::Response,
state::StateRepo
rect::Rect,
signal::SignalStore,
state::StateRepo,
text::{FontHandle, TextMeasure},
UiInstance,
};
#[cfg(feature = "builtin_elements")]
mod builtin {
pub mod rect;
pub mod container;
pub mod spacer;
pub mod progress_bar;
pub mod text;
}
#[cfg(feature = "builtin_elements")]
mod builtin;
pub use builtin::*;
pub trait UiElement {
fn name(&self) -> &'static str { "UiElement" }
fn state_id(&self) -> Option<u64> { None }
fn is_stateful(&self) -> bool { self.state_id().is_some() }
fn is_stateless(&self) -> bool { self.state_id().is_none() }
fn init_state(&self) -> Option<Box<dyn Any>> { None }
fn measure(&self, state: &StateRepo, layout: &LayoutInfo) -> Response;
fn process(&self, measure: &Response, state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands);
/// Context for the `Element::measure` function
pub struct MeasureContext<'a> {
pub layout: &'a LayoutInfo,
pub state: &'a StateRepo,
pub text_measure: TextMeasure<'a>,
pub current_font: FontHandle,
pub images: ImageCtx<'a>,
//XXX: should measure have a reference to input?
//pub input: InputCtx<'a>,
}
/// Context for the `Element::process` function
pub struct ProcessContext<'a> {
pub measure: &'a Response,
pub layout: &'a LayoutInfo,
pub draw: &'a mut UiDrawCommandList,
pub state: &'a mut StateRepo,
pub text_measure: TextMeasure<'a>,
pub current_font: FontHandle,
pub images: ImageCtx<'a>,
pub input: InputCtx<'a>,
pub signal: &'a mut SignalStore,
}
pub trait UiElement {
/// Get the name of the element (in lower case)
///
/// For example, "button" or "progress_bar"
fn name(&self) -> &'static str;
/// Get the requested UiElement size
///
/// You should implement this function whenever possible, otherwise some features may not work at all, such as the `Remaining` size
fn size(&self) -> Option<Size2d> { None }
/// Measure step, guaranteed to be called before the `process` step\
/// May be called multiple times per single frame, so it should not contain any expensive calls\
/// This function may not mutate any state.\
///
/// This function should return the size of the element along with any hints or layout metadata
fn measure(&self, ctx: MeasureContext) -> Response;
/// Process step, guaranteed to be called after the `measure` step\
/// You should process the user inputs and render the element here.
fn process(&self, ctx: ProcessContext);
}
/// A list of elements\
/// Use the [`add`](`ElementList::add`) method to add elements to the list
pub struct ElementList(pub Vec<Box<dyn UiElement>>);
impl ElementList {
/// Add an element to the list
pub fn add(&mut self, element: impl UiElement + 'static) {
self.0.push(Box::new(element))
}
/// Create a new `ElementList` from a callback\
/// The callback will be called with a reference to the newly list
pub(crate) fn from_callback(cb: impl FnOnce(&mut ElementList)) -> Self {
let mut list = ElementList(Vec::new());
cb(&mut list);
list
}
}
/// Extension trait for [`UiElement`] that adds the [`add_child`] and [`add_root`] methods
pub trait UiElementExt: UiElement {
/// Add element as a child/nested element.
fn add_child(self, ui: &mut ElementList);
/// Add element as a ui root.
fn add_root(self, ui: &mut UiInstance, max_size: impl Into<Rect>);
}
impl<T: UiElement + 'static> UiElementExt for T {
fn add_child(self, ui: &mut ElementList) {
ui.add(self)
}
fn add_root(self, ui: &mut UiInstance, rect: impl Into<Rect>) {
ui.add(self, rect);
}
}

View file

@ -0,0 +1,43 @@
// Layout stuff:
#[cfg(feature = "el_container")]
pub mod container;
#[cfg(feature = "el_frame_view")]
pub mod frame_view;
#[cfg(feature = "el_spacer")]
pub mod spacer;
#[cfg(feature = "el_br")]
pub mod br;
// Basic elements:
#[cfg(feature = "el_text")]
pub mod text;
#[cfg(feature = "el_image")]
pub mod image;
// "Extras":
// (meant to be replaced if needed)
#[cfg(feature = "el_progress_bar")]
pub mod progress_bar;
#[cfg(feature = "el_slider")]
pub mod slider;
// Wrappers:
#[cfg(feature = "el_transformer")]
pub mod transformer;
#[cfg(feature = "el_interactable")]
pub mod interactable;
//TODO add: Image
//TODO add: OverlayContainer (for simply laying multiple elements on top of each other)
//TODO add: Button, Checkbox, Dropdown, Input, Radio, Slider, Textarea, Toggle, etc.
//TODO add: some sort of "flexible" container (like a poor man's flexbox)

View file

@ -0,0 +1,22 @@
use crate::{
element::{MeasureContext, ProcessContext, UiElement},
measure::Response
};
#[derive(Clone, Copy, Debug, Default)]
pub struct Break;
impl UiElement for Break {
fn name(&self) -> &'static str {
"break"
}
fn measure(&self, _: MeasureContext) -> Response {
Response {
should_wrap: true,
..Default::default()
}
}
fn process(&self, _: ProcessContext) {}
}

View file

@ -1,237 +1,525 @@
use glam::{Vec2, vec2, Vec4};
//! a container element that can hold and layout multiple children elements
use derive_setters::Setters;
use glam::{Vec2, vec2};
use crate::{
UiDirection,
UiSize,
LayoutInfo,
draw::{UiDrawCommand, UiDrawCommands},
measure::{Response, Hints},
state::StateRepo,
element::UiElement
element::{ElementList, MeasureContext, ProcessContext, UiElement},
frame::{Frame, RectFrame},
layout::{compute_size, Alignment, Alignment2d, Direction, LayoutInfo, Size, Size2d, WrapBehavior},
measure::{Hints, Response},
rect::Sides,
};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Alignment {
Begin,
Center,
End,
//XXX: add Order/Direction::Forward/Reverse or sth?
//TODO: clip children flag
//TODO: borders
//TODO: min/max size
#[derive(Clone, Copy)]
struct CudLine {
start_idx: usize,
content_size: Vec2,
remaining_space: f32,
}
pub struct Border {
pub color: Vec4,
pub width: f32,
}
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
pub struct Sides<T> {
pub top: T,
pub bottom: T,
pub left: T,
pub right: T,
}
impl<T: Clone> Sides<T> {
#[inline]
pub fn all(value: T) -> Self {
Self {
top: value.clone(),
bottom: value.clone(),
left: value.clone(),
right: value,
}
}
#[inline]
pub fn horizontal_vertical(horizontal: T, vertical: T) -> Self {
Self {
top: vertical.clone(),
bottom: vertical,
left: horizontal.clone(),
right: horizontal,
}
}
struct ContainerUserData {
lines: Vec<CudLine>,
}
/// A container element that can hold and layout multiple children elements
#[derive(Setters)]
#[setters(prefix = "with_")]
pub struct Container {
// pub min_size: (UiSize, UiSize),
// pub max_size: (UiSize, UiSize),
pub size: (UiSize, UiSize),
pub direction: UiDirection,
//pub reverse: bool,
/// Size of the container
#[setters(into)]
pub size: Size2d,
/// Layout direction (horizontal/vertical)
pub direction: Direction,
//XXX: should we have separate gap value for primary and secondary (when wrapped, between lines of elements) axis?
/// Gap between children elements
pub gap: f32,
/// Padding inside the container (distance from the edges to the children elements)
#[setters(into)]
pub padding: Sides<f32>,
///Primary/secondary axis
pub align: (Alignment, Alignment),
pub background: Option<Vec4>,
pub borders: Sides<Option<Border>>,
pub clip: bool,
pub elements: Vec<Box<dyn UiElement>>,
/// Alignment of the children elements on X and Y axis
#[setters(into)]
pub align: Alignment2d,
#[setters(skip)]
pub background_frame: Box<dyn Frame>,
/// Controls if wrapping is enabled
#[setters(into)]
pub wrap: WrapBehavior,
/// List of children elements
#[setters(skip)]
pub children: ElementList,
}
impl Container {
pub fn with_children(mut self, ui: impl FnOnce(&mut ElementList)) -> Self {
self.children.0.extend(ElementList::from_callback(ui).0);
self
}
pub fn with_background(mut self, frame: impl Frame + 'static) -> Self {
self.background_frame = Box::new(frame);
self
}
}
impl Default for Container {
fn default() -> Self {
Self {
// min_size: (UiSize::Auto, UiSize::Auto),
// max_size: (UiSize::Auto, UiSize::Auto),
size: (UiSize::Auto, UiSize::Auto),
direction: UiDirection::Vertical,
//reverse: false,
size: (Size::Auto, Size::Auto).into(),
direction: Direction::Vertical,
gap: 0.,
padding: Sides::all(0.),
align: (Alignment::Begin, Alignment::Begin),
background: Default::default(),
borders: Default::default(),
clip: Default::default(),
elements: Vec::new(),
align: Alignment2d::default(),
background_frame: Box::<RectFrame>::default(),
wrap: WrapBehavior::Allow,
children: ElementList(Vec::new()),
}
}
}
impl Container {
pub fn measure_max_inner_size(&self, layout: &LayoutInfo) -> Vec2 {
let outer_size_x = match self.size.0 {
UiSize::Auto => layout.max_size.x,
UiSize::Percentage(p) => layout.max_size.x * p,
UiSize::Pixels(p) => p,
};
let outer_size_y = match self.size.1 {
UiSize::Auto => layout.max_size.y,
UiSize::Percentage(p) => layout.max_size.y * p,
UiSize::Pixels(p) => p,
};
// let outer_size_x = match self.size.width {
// Size::Auto => layout.max_size.x,
// Size::Relative(p) => layout.max_size.x * p,
// Size::Absolute(p) => p,
// Size::Remaining(p) => match layout.direction {
// Direction::Horizontal => layout.remaining_space.unwrap_or(layout.max_size.x) * p,
// Direction::Vertical => layout.max_size.x,
// }
// };
// let outer_size_y = match self.size.height {
// Size::Auto => layout.max_size.y,
// Size::Relative(p) => layout.max_size.y * p,
// Size::Absolute(p) => p,
// };
let outer_size = compute_size(layout, self.size, layout.max_size);
vec2(
outer_size_x - (self.padding.left + self.padding.right),
outer_size_y - (self.padding.top + self.padding.bottom),
outer_size.x - (self.padding.left + self.padding.right),
outer_size.y - (self.padding.top + self.padding.bottom),
)
}
}
impl UiElement for Container {
fn measure(&self, state: &StateRepo, layout: &LayoutInfo) -> Response {
let mut size = Vec2::ZERO;
//if matches!(self.size.0, UiSize::Auto) || matches!(self.size.1, UiSize::Auto) {
fn name(&self) -> &'static str {
"container"
}
fn size(&self) -> Option<Size2d> {
Some(self.size)
}
fn measure(&self, ctx: MeasureContext) -> Response {
// XXX: If both axes are NOT set to auto, we should be able quickly return the size
// ... but we can't, because we need to measure the children to get the inner_content_size and user_data values
// this is a potential optimization opportunity, maybe we could postpone this to the process call
// as it's guaranteed to be called only ONCE, while measure is assumed to be cheap and called multiple times
// ... we could also implement some sort of "global" caching for the measure call (to prevent traversal of the same tree multiple times),
// but that's a bit more complex and probably impossible with the current design of the measure/process calls
// In case wrapping is enabled, elements cannot exceed this size on the primary axis
let max_line_pri = match self.direction {
Direction::Horizontal => match self.size.width {
Size::Auto => ctx.layout.max_size.x,
Size::Relative(p) => ctx.layout.max_size.x * p,
Size::Absolute(p) => p,
Size::Remaining(p) => ctx.layout.remaining_space.unwrap_or(ctx.layout.max_size.x) * p,
},
Direction::Vertical => match self.size.height {
Size::Auto => ctx.layout.max_size.y,
Size::Relative(p) => ctx.layout.max_size.y * p,
Size::Absolute(p) => p,
Size::Remaining(p) => ctx.layout.remaining_space.unwrap_or(ctx.layout.max_size.y) * p,
}
};
//size of AABB containing all lines
let mut total_size = Vec2::ZERO;
//Size of the current row/column (if wrapping)
let mut line_size = Vec2::ZERO;
//Size of previous sec. axes combined
//(basically, in case of the horizontal layout, this is the height of the tallest element in the line)
//This is a vec2, but only one axis is used, depending on the layout direction
let mut line_sec_offset: Vec2 = Vec2::ZERO;
//Amount of elements in the current line
let mut line_element_count = 0;
//Leftover gap from the previous element on the primary axis
let mut leftover_gap = Vec2::ZERO;
for element in &self.elements {
let measure = element.measure(state, &LayoutInfo {
position: layout.position + size,
max_size: self.measure_max_inner_size(layout), // - size TODO
direction: self.direction,
});
match self.direction {
UiDirection::Horizontal => {
size.x += measure.size.x + self.gap;
size.y = size.y.max(measure.size.y);
leftover_gap.x = self.gap;
//line metadata for the user_data
let mut lines = vec![
CudLine {
start_idx: 0,
content_size: Vec2::ZERO,
remaining_space: 0.,
}
];
//set to true if in the current line there is an element with Remaining size (line will have to be wrapped)
// let mut has_remaining = false;
for (idx, element) in self.children.0.iter().enumerate() {
if let Some(esize) = element.size() {
let pri_size = match self.direction {
Direction::Horizontal => esize.width,
Direction::Vertical => esize.height,
};
if matches!(pri_size, Size::Remaining(_)) {
//XXX: kinda a hack?
continue;
}
}
let measure = element.measure(MeasureContext{
state: ctx.state,
layout: &LayoutInfo {
//XXX: if the element gets wrapped, this will be inaccurate.
//But, we cant know the size of the line until we measure it, and also
//We dont make any guarantees about this value being valid during the `measure` call
//For all intents and purposes, this is just a *hint* for the element to use
//(and could be just set to 0 for all we care)
position: ctx.layout.position + line_size + line_sec_offset,
//TODO: subtract size already taken by previous children
max_size: self.measure_max_inner_size(ctx.layout),
direction: self.direction,
remaining_space: None,
},
UiDirection::Vertical => {
size.x = size.x.max(measure.size.x);
size.y += measure.size.y + self.gap;
leftover_gap.y = self.gap;
text_measure: ctx.text_measure,
current_font: ctx.current_font,
images: ctx.images,
});
//Check the position of the side of element closest to the end on the primary axis
let end_pos_pri = match self.direction {
Direction::Horizontal => line_size.x + measure.size.x + self.padding.left + self.padding.right,
Direction::Vertical => line_size.y + measure.size.y + self.padding.top + self.padding.bottom,
};
//Wrap the element if it exceeds container's size and is not the first element in the line
let should_wrap_overflow = self.wrap.is_enabled() && (end_pos_pri > max_line_pri);
if self.wrap.is_allowed() && line_element_count > 0 && (measure.should_wrap || should_wrap_overflow) {
// >>>>>>> WRAP THAT B*TCH!
//Negate the leftover gap from the previous element
line_size -= leftover_gap;
//update the previous line metadata
{
let last_line = lines.last_mut().unwrap();
last_line.content_size = line_size;
//HACK: why? - self.gap, may be different for the last element or if it's the only element in the line
let will_produce_gap = if line_element_count > 1 { self.gap } else { 0. };
last_line.remaining_space = max_line_pri - will_produce_gap - match self.direction {
Direction::Horizontal => line_size.x + self.padding.left + self.padding.right,
Direction::Vertical => line_size.y + self.padding.top + self.padding.bottom,
};
}
//push the line metadata
lines.push(CudLine {
start_idx: idx,
content_size: Vec2::ZERO,
remaining_space: 0.,
});
//Update the total size accordingly
match self.direction {
Direction::Horizontal => {
total_size.x = total_size.x.max(line_size.x);
total_size.y += line_size.y + self.gap;
},
Direction::Vertical => {
total_size.x += line_size.x + self.gap;
total_size.y = total_size.y.max(line_size.y);
}
}
//Now, update line_sec_offset
match self.direction {
Direction::Horizontal => {
line_sec_offset.y += measure.size.y + self.gap;
},
Direction::Vertical => {
line_sec_offset.x += measure.size.x + self.gap;
}
};
//Reset the line size and element count
line_size = Vec2::ZERO;
line_element_count = 0;
}
//Increment element count
line_element_count += 1;
//Sset the leftover gap in case this is the last element in the line
match self.direction {
Direction::Horizontal => {
line_size.x += measure.size.x + self.gap;
line_size.y = line_size.y.max(measure.size.y);
leftover_gap = vec2(self.gap, 0.);
},
Direction::Vertical => {
line_size.x = line_size.x.max(measure.size.x);
line_size.y += measure.size.y + self.gap;
leftover_gap = vec2(0., self.gap);
}
}
}
size -= leftover_gap;
let inner_content_size = Some(size);
line_size -= leftover_gap;
size += vec2(
//Update the content size of the last line
{
//HACK: why? - self.gap, may be different for the last element or if it's the only element in the line
let cur_line = lines.last_mut().unwrap();
cur_line.content_size = line_size;
let will_produce_gap = if line_element_count > 1 { self.gap } else { 0. };
cur_line.remaining_space = max_line_pri - will_produce_gap - match self.direction {
Direction::Horizontal => line_size.x + self.padding.left + self.padding.right,
Direction::Vertical => line_size.y + self.padding.top + self.padding.bottom,
};
}
//Update the total size according to the size of the last line
match self.direction {
Direction::Horizontal => {
total_size.x = total_size.x.max(line_size.x);
total_size.y += line_size.y;
},
Direction::Vertical => {
total_size.x += line_size.x;
total_size.y = total_size.y.max(line_size.y);
}
}
//Now, total_size should hold the size of the AABB containing all lines
//This is exactly what inner_content_size hint should be set to
let inner_content_size = Some(total_size);
//After setting the inner_content_size, we can calculate the size of the container
//Including padding, and in case the size is set to non-auto, override the size
total_size += vec2(
self.padding.left + self.padding.right,
self.padding.top + self.padding.bottom,
);
match self.size.0 {
UiSize::Auto => (),
UiSize::Percentage(percentage) => size.x = layout.max_size.x * percentage,
UiSize::Pixels(pixels) => size.x = pixels,
let computed_size = compute_size(ctx.layout, self.size, total_size);
match self.size.width {
Size::Auto => (),
_ => total_size.x = computed_size.x,
}
match self.size.1 {
UiSize::Auto => (),
UiSize::Percentage(percentage) => size.y = layout.max_size.y * percentage,
UiSize::Pixels(pixels) => size.y = pixels,
match self.size.height {
Size::Auto => (),
_ => total_size.y = computed_size.y,
}
// match self.size.width {
// Size::Auto => (),
// Size::Relative(percentage) => total_size.x = ctx.layout.max_size.x * percentage,
// Size::Absolute(pixels) => total_size.x = pixels,
// }
// match self.size.height {
// Size::Auto => (),
// Size::Relative(percentage) => total_size.y = ctx.layout.max_size.y * percentage,
// Size::Absolute(pixels) => total_size.y = pixels,
// }
Response {
size,
size: total_size,
hints: Hints {
inner_content_size,
..Default::default()
},
user_data: None
user_data: Some(Box::new(ContainerUserData { lines })),
..Default::default()
}
}
fn process(&self, measure: &Response, state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands) {
let mut position = layout.position;
fn process(&self, ctx: ProcessContext) {
let user_data: &ContainerUserData = ctx.measure.user_data
.as_ref().expect("no user data attached to container")
.downcast_ref().expect("invalid user data type");
let mut position = ctx.layout.position;
//background
if let Some(color) = self.background {
draw.add(UiDrawCommand::Rectangle {
position,
size: measure.size,
color
});
}
// if !self.background.is_transparent() {
// let corner_colors = self.background.corners();
// ctx.draw.add(UiDrawCommand::Rectangle {
// position,
// size: ctx.measure.size,
// color: corner_colors,
// texture: self.background_image,
// rounded_corners: (self.corner_radius.max_f32() > 0.).then_some({
// RoundedCorners::from_radius(self.corner_radius)
// }),
// });
// }
self.background_frame.draw(ctx.draw, (ctx.layout.position, ctx.measure.size).into());
//padding
position += vec2(self.padding.left, self.padding.top);
//alignment
match (self.align.0, self.direction) {
(Alignment::Begin, _) => (),
(Alignment::Center, UiDirection::Horizontal) => {
position.x += (measure.size.x - measure.hints.inner_content_size.unwrap().x) / 2.;
},
(Alignment::Center, UiDirection::Vertical) => {
position.y += (measure.size.y - measure.hints.inner_content_size.unwrap().y) / 2.;
},
(Alignment::End, UiDirection::Horizontal) => {
position.x += measure.size.x - measure.hints.inner_content_size.unwrap().x - self.padding.right - self.padding.left;
},
(Alignment::End, UiDirection::Vertical) => {
position.y += measure.size.y - measure.hints.inner_content_size.unwrap().y - self.padding.bottom - self.padding.top;
}
}
//convert alignment to pri/sec axis based
//.0 = primary, .1 = secondary
let pri_sec_align = match self.direction {
Direction::Horizontal => (self.align.horizontal, self.align.vertical),
Direction::Vertical => (self.align.vertical, self.align.horizontal),
};
for element in &self.elements {
//(passing max size from layout rather than actual bounds for the sake of consistency with measure() above)
//alignment (on sec. axis)
// match pri_sec_align.1 {
// Alignment::Begin => (),
// Alignment::Center => {
// position += match self.direction {
// UiDirection::Horizontal => vec2(0., (ctx.measure.size.y - self.padding.top - self.padding.bottom - user_data.lines.last().unwrap().content_size.y) / 2.),
// UiDirection::Vertical => vec2((ctx.measure.size.x - self.padding.left - self.padding.right - user_data.lines.last().unwrap().content_size.x) / 2., 0.),
// };
// },
// Alignment::End => {
// position += match self.direction {
// UiDirection::Horizontal => vec2(0., ctx.measure.size.y - user_data.lines.last().unwrap().content_size.y - self.padding.bottom - self.padding.top),
// UiDirection::Vertical => vec2(ctx.measure.size.x - user_data.lines.last().unwrap().content_size.x - self.padding.right - self.padding.left, 0.),
// };
// }
// }
let mut el_layout = LayoutInfo {
position,
max_size: self.measure_max_inner_size(layout),
direction: self.direction,
};
for (line_idx, cur_line) in user_data.lines.iter().enumerate() {
let mut local_position = position;
//measure
let el_measure = element.measure(state, &el_layout);
//align (on sec. axis)
match (self.align.1, self.direction) {
//alignment on primary axis
match (pri_sec_align.0, self.direction) {
(Alignment::Begin, _) => (),
(Alignment::Center, UiDirection::Horizontal) => {
el_layout.position.y += (measure.size.y - self.padding.bottom - self.padding.top - el_measure.size.y) / 2.;
(Alignment::Center, Direction::Horizontal) => {
local_position.x += (ctx.measure.size.x - cur_line.content_size.x) / 2. - self.padding.left;
},
(Alignment::Center, UiDirection::Vertical) => {
el_layout.position.x += (measure.size.x - self.padding.left - self.padding.right - el_measure.size.x) / 2.;
(Alignment::Center, Direction::Vertical) => {
local_position.y += (ctx.measure.size.y - cur_line.content_size.y) / 2. - self.padding.top;
},
(Alignment::End, UiDirection::Horizontal) => {
el_layout.position.y += measure.size.y - el_measure.size.y - self.padding.bottom;
(Alignment::End, Direction::Horizontal) => {
local_position.x += ctx.measure.size.x - cur_line.content_size.x - self.padding.right - self.padding.left;
},
(Alignment::End, UiDirection::Vertical) => {
el_layout.position.x += measure.size.x - el_measure.size.x - self.padding.right;
(Alignment::End, Direction::Vertical) => {
local_position.y += ctx.measure.size.y - cur_line.content_size.y - self.padding.bottom - self.padding.top;
}
}
//process
element.process(&el_measure, state, &el_layout, draw);
let next_line_begin = user_data.lines
.get(line_idx + 1)
.map(|l| l.start_idx)
.unwrap_or(self.children.0.len());
//layout
for element_idx in cur_line.start_idx..next_line_begin {
let element = &self.children.0[element_idx];
//(passing max size from layout rather than actual known bounds for the sake of consistency with measure() above)
//... as this must match!
let mut el_layout = LayoutInfo {
position: local_position,
max_size: self.measure_max_inner_size(ctx.layout),
direction: self.direction,
remaining_space: Some(cur_line.remaining_space),
};
//measure
let el_measure = element.measure(MeasureContext {
layout: &el_layout,
state: ctx.state,
text_measure: ctx.text_measure,
current_font: ctx.current_font,
images: ctx.images,
});
//align (on sec. axis)
//TODO separate align withing the line and align of the whole line
let inner_content_size = ctx.measure.hints.inner_content_size.unwrap();
match (pri_sec_align.1, self.direction) {
(Alignment::Begin, _) => (),
(Alignment::Center, Direction::Horizontal) => {
//Align whole row
el_layout.position.y += ((ctx.measure.size.y - self.padding.bottom - self.padding.top) - inner_content_size.y) / 2.;
//Align within row
el_layout.position.y += (cur_line.content_size.y - el_measure.size.y) / 2.;
},
(Alignment::Center, Direction::Vertical) => {
//Align whole row
el_layout.position.x += ((ctx.measure.size.x - self.padding.left - self.padding.right) - inner_content_size.x) / 2.;
//Align within row
el_layout.position.x += (cur_line.content_size.x - el_measure.size.x) / 2.;
},
//TODO update these two cases:
(Alignment::End, Direction::Horizontal) => {
//Align whole row
el_layout.position.y += (ctx.measure.size.y - self.padding.bottom - self.padding.top) - inner_content_size.y;
//Align within row
el_layout.position.y += cur_line.content_size.y - el_measure.size.y;
},
(Alignment::End, Direction::Vertical) => {
//Align whole row
el_layout.position.x += (ctx.measure.size.x - self.padding.right - self.padding.left) - inner_content_size.x;
//Align within row
el_layout.position.x += cur_line.content_size.x - el_measure.size.x;
}
}
//process
element.process(ProcessContext {
measure: &el_measure,
layout: &el_layout,
draw: ctx.draw,
state: ctx.state,
text_measure: ctx.text_measure,
current_font: ctx.current_font,
images: ctx.images,
input: ctx.input,
signal: ctx.signal,
});
//layout
match self.direction {
Direction::Horizontal => {
local_position.x += el_measure.size.x + self.gap;
},
Direction::Vertical => {
local_position.y += el_measure.size.y + self.gap;
}
}
}
//Move to the next line
match self.direction {
UiDirection::Horizontal => {
position.x += el_measure.size.x + self.gap;
},
UiDirection::Vertical => {
position.y += el_measure.size.y + self.gap;
Direction::Horizontal => {
position.y += cur_line.content_size.y + self.gap;
//position.x -= cur_line.content_size.x;
// leftover_line_gap = vec2(0., self.gap);
}
}
Direction::Vertical => {
position.x += cur_line.content_size.x + self.gap;
//position.y -= cur_line.content_size.y;
// leftover_line_gap = vec2(self.gap, 0.);
}
};
}
}
}

View file

@ -0,0 +1,68 @@
//! Simple element that displays the specified frame
use derive_setters::Setters;
use crate::{
element::{MeasureContext, ProcessContext, UiElement},
frame::{Frame, RectFrame},
layout::{compute_size, Size2d},
measure::Response,
size
};
/// Simple rectangle that displays the specified frame
#[derive(Setters)]
#[setters(prefix = "with_")]
pub struct FrameView {
/// Size of the rectangle
#[setters(into)]
pub size: Size2d,
/// Frame
#[setters(skip)]
pub frame: Box<dyn Frame>,
}
impl FrameView {
pub fn new(frame: impl Frame + 'static) -> Self {
Self {
size: size!(10, 10),
frame: Box::new(frame),
}
}
//setters:
pub fn with_frame(mut self, frame: impl Frame + 'static) -> Self {
self.frame = Box::new(frame);
self
}
}
impl Default for FrameView {
fn default() -> Self {
Self {
size: size!(10, 10),
frame: Box::new(RectFrame::color((0., 0., 0., 0.5))),
}
}
}
impl UiElement for FrameView {
fn name(&self) -> &'static str {
"frame_view"
}
fn size(&self) -> Option<Size2d> {
Some(self.size)
}
fn measure(&self, ctx: MeasureContext) -> Response {
Response {
size: compute_size(ctx.layout, self.size, ctx.layout.max_size),
..Default::default()
}
}
fn process(&self, ctx: ProcessContext) {
self.frame.draw(ctx.draw, (ctx.layout.position, ctx.measure.size).into());
}
}

View file

@ -0,0 +1,93 @@
use derive_setters::Setters;
use glam::vec2;
use crate::{
draw::{ImageHandle, RoundedCorners, UiDrawCommand},
element::{MeasureContext, ProcessContext, UiElement},
layout::{compute_size, Size, Size2d},
measure::Response,
rect::{Corners, FillColor},
};
#[derive(Setters)]
#[setters(prefix = "with_")]
pub struct Image {
/// Image handle to draw
#[setters(skip)]
pub image: ImageHandle,
/// Size of the image.
///
/// - If one of the dimensions is `Size::Auto`, the image will be scaled to fit the other dimension\
/// (aspect ratio is preserved)
/// - If both dimensions are `Size::Auto`, the image will be drawn at its original size
/// - All other values behave as expected
#[setters(into)]
pub size: Size2d,
/// Color of the image
///
/// Image will get multiplied/tinted by this color or gradient
#[setters(into)]
pub color: FillColor,
/// Corner radius of the image
#[setters(into)]
pub corner_radius: Corners<f32>,
}
impl Image {
pub fn new(handle: ImageHandle) -> Self {
Self {
image: handle,
size: Size2d {
width: Size::Auto,
height: Size::Auto,
},
color: (1., 1., 1.).into(),
corner_radius: Corners::all(0.),
}
}
}
impl UiElement for Image {
fn name(&self) -> &'static str {
"image"
}
fn size(&self) -> Option<Size2d> {
Some(self.size)
}
fn measure(&self, ctx: MeasureContext) -> Response {
let dim = ctx.images.get_size(self.image).expect("invalid image handle");
let pre_size = compute_size(ctx.layout, self.size, dim.as_vec2());
Response {
size: compute_size(ctx.layout, self.size, vec2(
match self.size.height {
Size::Auto => dim.x as f32,
_ => (pre_size.y / dim.y as f32) * dim.x as f32,
},
match self.size.height {
Size::Auto => dim.x as f32,
_ => (pre_size.y / dim.y as f32) * dim.x as f32,
},
)),
..Default::default()
}
}
fn process(&self, ctx: ProcessContext) {
if !self.color.is_transparent() {
ctx.draw.add(UiDrawCommand::Rectangle {
position: ctx.layout.position,
size: ctx.measure.size,
color: self.color.corners(),
texture: Some(self.image),
texture_uv: None,
rounded_corners: (self.corner_radius.max_f32() > 0.).then_some({
RoundedCorners::from_radius(self.corner_radius)
}),
});
}
}
}

View file

@ -0,0 +1,109 @@
//! wrapper that allows adding click and hover events to any element
// not sure if this is a good idea...
// but having the ability to add a click event to any element would be nice, and this is a naive way to do it
use crate::{
element::{MeasureContext, ProcessContext, UiElement},
signal::{trigger::SignalTrigger, Signal},
};
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum InteractableEvent {
#[default]
Click,
Hover,
Active,
}
/// Wrapper that allows adding click and hover events to any element
pub struct Interactable {
/// The wrapped element that will be interactable
pub element: Box<dyn UiElement>,
/// Event to listen for
pub event: InteractableEvent,
/// Signal that will be called if the element was clicked in the current frame
pub signal: SignalTrigger,
}
impl Interactable {
pub fn new<S: Signal, F: Fn() -> S + 'static>(
element: Box<dyn UiElement>,
event: InteractableEvent,
signal: F
) -> Self {
Self {
element,
event,
signal: SignalTrigger::new(signal),
}
}
}
impl UiElement for Interactable {
fn name(&self) -> &'static str {
"interactable"
}
fn size(&self) -> Option<crate::layout::Size2d> {
self.element.size()
}
fn measure(&self, ctx: MeasureContext) -> crate::measure::Response {
self.element.measure(ctx)
}
fn process(&self, ctx: ProcessContext) {
let rect = ctx.measure.rect(ctx.layout.position);
//XXX: should we do this AFTER normal process call of wrapped element?
let event_happened = match self.event {
//TODO: actually pass the response
InteractableEvent::Click => ctx.input.check_click(rect).is_some(),
InteractableEvent::Hover => ctx.input.check_hover(rect),
InteractableEvent::Active => ctx.input.check_active(rect).is_some(),
};
if event_happened {
self.signal.fire(ctx.signal);
}
self.element.process(ctx)
}
}
/// Extension trait for [`UiElement`] that adds methods to wrap the element in an [`Interactable`]
pub trait ElementInteractableExt: UiElement {
/// Wrap the element in an [`Interactable`] that will call the given signal when the specified event occurs
fn into_interactable<S: Signal, F: Fn() -> S + 'static>(self, event: InteractableEvent, signal: F) -> Interactable;
/// Wrap the element in an [`Interactable`] that will call the given signal when clicked
fn on_click<S: Signal, F: Fn() -> S + 'static>(self, signal: F) -> Interactable;
/// Wrap the element in an [`Interactable`] that will call the given signal continuously while hovered
fn on_hover<S: Signal, F: Fn() -> S + 'static>(self, signal: F) -> Interactable;
/// Wrap the element in an [`Interactable`] that will call the given signal continuously while active
fn on_active<S: Signal, F: Fn() -> S + 'static>(self, signal: F) -> Interactable;
}
impl<T: UiElement + 'static> ElementInteractableExt for T {
fn into_interactable<S: Signal, F: Fn() -> S + 'static>(self, event: InteractableEvent, signal: F) -> Interactable {
Interactable::new(Box::new(self), event, signal)
}
fn on_click<S: Signal, F: Fn() -> S + 'static>(self, signal: F) -> Interactable {
self.into_interactable(InteractableEvent::Click, signal)
}
fn on_hover<S: Signal, F: Fn() -> S + 'static>(self, signal: F) -> Interactable {
self.into_interactable(InteractableEvent::Hover, signal)
}
fn on_active<S: Signal, F: Fn() -> S + 'static>(self, signal: F) -> Interactable {
self.into_interactable(InteractableEvent::Active, signal)
}
}

View file

@ -1,70 +1,120 @@
use glam::{vec2, Vec4, vec4};
use derive_setters::Setters;
use glam::vec2;
use crate::{
UiSize, LayoutInfo,
draw::{UiDrawCommand, UiDrawCommands},
element::{MeasureContext, ProcessContext, UiElement},
frame::{Frame, RectFrame},
layout::{compute_size, Size, Size2d},
measure::Response,
state::StateRepo,
element::UiElement
};
#[derive(Debug, Clone, Copy)]
//TODO: Use Frames here instead of FillColor
#[derive(Setters)]
#[setters(prefix = "with_")]
pub struct ProgressBar {
pub size: (UiSize, UiSize),
/// Current progress, should be in the range 0.0..=1.0
pub value: f32,
pub color_foreground: Vec4,
pub color_background: Vec4,
/// Size of the progress bar element
#[setters(into)]
pub size: Size2d,
/// Foreground (bar) color
#[setters(skip)]
pub foreground: Box<dyn Frame>,
/// Background color
#[setters(skip)]
pub background: Box<dyn Frame>,
}
impl ProgressBar {
pub const DEFAULT_HEIGHT: f32 = 20.0;
pub fn with_background(mut self, frame: impl Frame + 'static) -> Self {
self.background = Box::new(frame);
self
}
pub fn with_foreground(mut self, frame: impl Frame + 'static) -> Self {
self.foreground = Box::new(frame);
self
}
}
impl Default for ProgressBar {
fn default() -> Self {
Self {
size: (UiSize::Auto, UiSize::Auto),
value: 0.,
color_foreground: vec4(0.0, 0.0, 1.0, 1.0),
color_background: vec4(0.0, 0.0, 0.0, 1.0),
size: Size::Auto.into(),
foreground: Box::new(RectFrame::color((0.0, 0.0, 1.0, 1.0))),
background: Box::new(RectFrame::color((0.0, 0.0, 0.0, 1.0))),
}
}
}
const BAR_HEIGHT: f32 = 20.0;
impl UiElement for ProgressBar {
fn name(&self) -> &'static str { "Progress bar" }
fn name(&self) -> &'static str {
"progress_bar"
}
fn measure(&self, _: &StateRepo, layout: &LayoutInfo) -> Response {
fn measure(&self, ctx: MeasureContext) -> Response {
Response {
size: vec2(
match self.size.0 {
UiSize::Auto => layout.max_size.x.max(300.),
UiSize::Percentage(p) => layout.max_size.x * p,
UiSize::Pixels(p) => p,
},
match self.size.1 {
UiSize::Auto => BAR_HEIGHT,
UiSize::Percentage(p) => layout.max_size.y * p,
UiSize::Pixels(p) => p,
}
),
size: compute_size(ctx.layout, self.size, vec2(
ctx.layout.max_size.x.max(300.), //XXX: remove .max(300)?
Self::DEFAULT_HEIGHT,
)),
hints: Default::default(),
user_data: None,
..Default::default()
}
}
fn process(&self, measure: &Response, state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands) {
fn process(&self, ctx: ProcessContext) {
let value = self.value.clamp(0., 1.);
if value < 1. {
draw.add(UiDrawCommand::Rectangle {
position: layout.position,
size: measure.size,
color: self.color_background
});
//FIXME: these optimizations may not be valid
if value < 1. || !self.foreground.covers_opaque() {
self.background.draw(ctx.draw, (ctx.layout.position, ctx.measure.size).into());
}
if value > 0. {
draw.add(UiDrawCommand::Rectangle {
position: layout.position,
size: measure.size * vec2(value, 1.0),
color: self.color_foreground
});
self.foreground.draw(ctx.draw, (ctx.layout.position, ctx.measure.size * vec2(value, 1.)).into());
}
// let rounded_corners =
// (self.corner_radius.max_f32() > 0.).then_some({
// //HACK: fix clipping issues; //todo: get rid of this
// let mut radii = self.corner_radius;
// let width = ctx.measure.size.x * value;
// if width <= radii.max_f32() * 2. {
// radii.bottom_right = 0.;
// radii.top_right = 0.;
// }
// if width <= radii.max_f32() {
// radii.bottom_left = 0.;
// radii.top_left = 0.;
// }
// RoundedCorners::from_radius(radii)
// });
// if value < 1. {
// ctx.draw.add(UiDrawCommand::Rectangle {
// position: ctx.layout.position,
// size: ctx.measure.size,
// color: self.background.corners(),
// texture: None,
// texture_uv: None,
// rounded_corners
// });
// }
// if value > 0. {
// ctx.draw.add(UiDrawCommand::Rectangle {
// position: ctx.layout.position,
// size: ctx.measure.size * vec2(value, 1.0),
// color: self.foreground.corners(),
// texture: None,
// texture_uv: None,
// rounded_corners,
// });
// }
}
}

View file

@ -1,54 +0,0 @@
use glam::{vec2, Vec4};
use crate::{
LayoutInfo,
UiSize,
element::UiElement,
state::StateRepo,
measure::Response,
draw::{UiDrawCommand, UiDrawCommands}
};
pub struct Rect {
pub size: (UiSize, UiSize),
pub color: Option<Vec4>,
}
impl Default for Rect {
fn default() -> Self {
Self {
size: (UiSize::Pixels(10.), UiSize::Pixels(10.)),
color: Some(Vec4::new(0., 0., 0., 0.5)),
}
}
}
impl UiElement for Rect {
fn measure(&self, _state: &StateRepo, layout: &LayoutInfo) -> Response {
Response {
size: vec2(
match self.size.0 {
UiSize::Auto => layout.max_size.x,
UiSize::Percentage(percentage) => layout.max_size.x * percentage,
UiSize::Pixels(pixels) => pixels,
},
match self.size.1 {
UiSize::Auto => layout.max_size.y,
UiSize::Percentage(percentage) => layout.max_size.y * percentage,
UiSize::Pixels(pixels) => pixels,
},
),
hints: Default::default(),
user_data: None
}
}
fn process(&self, measure: &Response, _state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands) {
if let Some(color) = self.color {
draw.add(UiDrawCommand::Rectangle {
position: layout.position,
size: measure.size,
color,
});
}
}
}

View file

@ -0,0 +1,221 @@
//! a slider element that allows selecting a value in a range
use derive_setters::Setters;
use glam::{Vec2, vec2};
use crate::{
draw::UiDrawCommand, element::{MeasureContext, ProcessContext, UiElement}, frame::{Frame, RectFrame}, layout::{compute_size, Size2d}, measure::Response, rect::FillColor, signal::{trigger::SignalTriggerArg, Signal}
};
//TODO: use state for slider?
// ^ useful if the user only hanldes the drag end event or has large step sizes with relative mode
//TODO: adopt frame api here
/// Follow mode for the slider
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum SliderFollowMode {
/// Slider will change based on the absolute mouse position in the slider
///
/// This is the default mode and is recommended for most use cases
#[default]
Absolute,
/// Slider will change based on the difference between the current and starting mouse position
///
/// This is an experimental option and does not currently work well for sliders with large step sizes
Relative,
}
/// A slider element that allows selecting a value in a range
#[derive(Setters)]
#[setters(prefix = "with_")]
pub struct Slider {
/// Value of the slider, should be in range 0..1
///
/// Out of range values will be clamped
pub value: f32,
/// Size of the element
#[setters(into)]
pub size: Size2d,
/// Track frame
#[setters(skip)]
pub track: Box<dyn Frame>,
/// Track active frame
#[setters(skip)]
pub track_active: Box<dyn Frame>,
/// Handle frame
#[setters(skip)]
pub handle: Box<dyn Frame>,
/// Track height *relative to the slider height*\
///
/// Range: 0.0..=1.0
pub track_height: f32,
/// Handle size
///
/// Please be aware that:
///
/// - Width is *static* and specified in *pixels* (e.g. `15.0`)
/// - Height is *relative* to the slider height,\
/// ...and is specified as a *ratio* in range `0.0..=1.0`
pub handle_size: (f32, f32),
/// Follow mode
pub follow_mode: SliderFollowMode,
#[setters(skip)]
pub on_change: Option<SignalTriggerArg<f32>>,
}
impl Default for Slider {
fn default() -> Self {
Self {
value: 0.0,
size: Size2d::default(),
handle: Box::new(RectFrame::color((0.0, 0.0, 1.))),
track: Box::new(RectFrame::color((0.5, 0.5, 0.5))),
track_active: Box::new(RectFrame::color((0.0, 0.0, 0.75))),
track_height: 0.25,
handle_size: (15.0, 1.),
follow_mode: SliderFollowMode::default(),
on_change: None
}
}
}
impl Slider {
pub const DEFAULT_HEIGHT: f32 = 20.0;
pub fn new(value: f32) -> Self {
Self {
value,
..Default::default()
}
}
pub fn on_change<S: Signal, T: Fn(f32) -> S + 'static>(self, f: T) -> Self {
Self {
on_change: Some(SignalTriggerArg::new(f)),
..self
}
}
pub fn with_track(mut self, track: impl Frame + 'static) -> Self {
self.track = Box::new(track);
self
}
pub fn with_track_active(mut self, track_active: impl Frame + 'static) -> Self {
self.track_active = Box::new(track_active);
self
}
pub fn with_handle(mut self, handle: impl Frame + 'static) -> Self {
self.handle = Box::new(handle);
self
}
}
impl UiElement for Slider {
fn name(&self) -> &'static str {
"slider"
}
fn measure(&self, ctx: MeasureContext) -> Response {
Response {
size: compute_size(ctx.layout, self.size, (ctx.layout.max_size.x, Self::DEFAULT_HEIGHT).into()),
..Default::default()
}
}
fn process(&self, ctx: ProcessContext) {
//XXX: some of these assumptions are wrong if the corners are rounded
//Compute handle size:
// This is kinda counter-intuitive, but if the handle is transparent, we treat it as completely disabled
// To prevent confusing offset from the edge of the slider, we set the handle size to 0
// let handle_size = if self.handle_color.is_transparent() {
// Vec2::ZERO
// } else {
// vec2(15., ctx.measure.size.y)
// };
let handle_size = vec2(self.handle_size.0, self.handle_size.1 * ctx.measure.size.y);
//Draw the track
//If the active part is opaque and value >= 1., we don't need to draw the background as the active part will cover it
//However, if the handle is not opaque, we need to draw the background as the active part won't quite reach the end
//Of corse, if it's fully transparent, we don't need to draw it either
// if !(self.track_color.is_transparent() || (self.track_active_color.is_opaque() && self.handle_color.is_opaque() && self.value >= 1.)) {
if !(self.track_active.covers_opaque() && self.handle.covers_opaque() && (self.handle_size.1 >= self.track_height) && self.value >= 1.) {
self.track.draw(
ctx.draw,
(
ctx.layout.position + ctx.measure.size * vec2(0., 0.5 - self.track_height / 2.),
ctx.measure.size * vec2(1., self.track_height),
).into()
);
}
//"Active" part of the track
//We can skip drawing it if it's fully transparent or value <= 0.
//But if the handle is not opaque, it should be visible even if value is zero
// if !(self.track_active_color.is_transparent() || (self.value <= 0. && self.handle_color.is_opaque())) {
if !(self.handle.covers_opaque() && (self.handle_size.1 >= self.track_height) && self.value <= 0.) {
self.track_active.draw(
ctx.draw,
(
ctx.layout.position + ctx.measure.size * vec2(0., 0.5 - self.track_height / 2.),
(ctx.measure.size - handle_size * Vec2::X) * vec2(self.value, self.track_height) + handle_size * Vec2::X / 2.,
).into()
);
}
// The handle
// if handle_size.x != 0. && !self.handle_color.is_transparent() {
// let value = self.value.clamp(0., 1.);
// ctx.draw.add(UiDrawCommand::Rectangle {
// position: ctx.layout.position + ((ctx.measure.size.x - handle_size.x) * value) * Vec2::X,
// size: handle_size,
// color: self.handle_color.into(),
// texture: None,
// rounded_corners: None,
// });
// }
if (self.handle_size.0 > 0. && self.handle_size.1 > 0.) {
self.handle.draw(
ctx.draw,
(
ctx.layout.position +
((ctx.measure.size.x - handle_size.x) * self.value) * Vec2::X +
ctx.measure.size.y * ((1. - self.handle_size.1) * 0.5) * Vec2::Y,
handle_size,
).into()
);
}
//handle events
if let Some(res) = ctx.input.check_active(ctx.measure.rect(ctx.layout.position)) {
let new_value = match self.follow_mode {
SliderFollowMode::Absolute => {
((res.position_in_rect.x - handle_size.x / 2.) / (ctx.measure.size.x - handle_size.x)).clamp(0., 1.)
},
SliderFollowMode::Relative => {
let delta = res.position_in_rect.x - res.last_position_in_rect.x;
let delta_ratio = delta / (ctx.measure.size.x - handle_size.x);
(self.value + delta_ratio).clamp(0., 1.)
}
};
if let Some(signal) = &self.on_change {
signal.fire(ctx.signal, new_value);
}
//TODO call signal with new value
}
}
}

View file

@ -1,13 +1,14 @@
//! Adds spacing between elements in a layout
use glam::vec2;
use crate::{
LayoutInfo,
UiDirection,
element::UiElement,
state::StateRepo,
element::{MeasureContext, ProcessContext, UiElement},
measure::Response,
draw::{UiDrawCommand, UiDrawCommands}
layout::Direction
};
/// Adds spacing between elements in a layout\
/// (depending on the current layout direction)
pub struct Spacer(pub f32);
impl Default for Spacer {
@ -17,16 +18,19 @@ impl Default for Spacer {
}
impl UiElement for Spacer {
fn measure(&self, state: &StateRepo, layout: &LayoutInfo) -> Response {
fn name(&self) -> &'static str {
"spacer"
}
fn measure(&self, ctx: MeasureContext) -> Response {
Response {
size: match layout.direction {
UiDirection::Horizontal => vec2(self.0, 0.),
UiDirection::Vertical => vec2(0., self.0),
size: match ctx.layout.direction {
Direction::Horizontal => vec2(self.0, 0.),
Direction::Vertical => vec2(0., self.0),
},
hints: Default::default(),
user_data: None
..Default::default()
}
}
fn process(&self, _measure: &Response, _state: &mut StateRepo, _layout: &LayoutInfo, _draw: &mut UiDrawCommands) {}
fn process(&self, _ctx: ProcessContext) {}
}

View file

@ -1,61 +1,107 @@
//! simple text element, renders a string of text
use std::borrow::Cow;
use derive_setters::Setters;
use glam::{vec2, Vec4};
use crate::{
LayoutInfo,
UiSize,
element::UiElement,
state::StateRepo,
draw::UiDrawCommand,
element::{MeasureContext, ProcessContext, UiElement},
layout::{compute_size, Size, Size2d},
measure::Response,
draw::{UiDrawCommand, UiDrawCommands}, text::FontHandle
text::FontHandle,
};
//TODO: text fit
// pub enum TextSize {
// FitToWidthRatio(f32),
// FitToHeightRatio(f32),
// Constant(u8),
// }
/// Simple text element, renders a string of text
#[derive(Setters)]
#[setters(prefix = "with_")]
pub struct Text {
/// Text to render
#[setters(into)]
pub text: Cow<'static, str>,
pub size: (UiSize, UiSize),
/// Size of the text element
#[setters(into)]
pub size: Size2d,
/// Color of the text
#[setters(into)]
pub color: Vec4,
pub font: FontHandle,
pub text_size: u8,
/// Font to use for rendering the text\
/// If set to `None` either currently selected font or the default font will be used
#[setters(into)]
pub font: Option<FontHandle>,
/// Size of the text, in points (these are not pixels)
pub text_size: u16,
}
impl Default for Text {
fn default() -> Self {
Self {
text: "".into(),
size: (UiSize::Auto, UiSize::Auto),
size: (Size::Auto, Size::Auto).into(),
color: Vec4::new(1., 1., 1., 1.),
font: FontHandle(0),
font: None,
text_size: 16,
}
}
}
impl UiElement for Text {
fn measure(&self, _state: &StateRepo, layout: &LayoutInfo) -> Response {
Response {
size: vec2(
match self.size.0 {
UiSize::Auto => layout.max_size.x,
UiSize::Percentage(percentage) => layout.max_size.x * percentage,
UiSize::Pixels(pixels) => pixels,
},
match self.size.1 {
UiSize::Auto => self.text_size as f32,
UiSize::Percentage(percentage) => layout.max_size.y * percentage,
UiSize::Pixels(pixels) => pixels,
},
),
hints: Default::default(),
user_data: None
impl Text {
pub fn new(text: impl Into<Cow<'static, str>>) -> Self {
Self {
text: text.into(),
..Default::default()
}
}
fn process(&self, _measure: &Response, _state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands) {
draw.add(UiDrawCommand::Text {
fn font(&self, f: FontHandle) -> FontHandle {
self.font.unwrap_or(f)
}
}
impl UiElement for Text {
fn name(&self) -> &'static str {
"text"
}
fn size(&self) -> Option<Size2d> {
Some(self.size)
}
fn measure(&self, ctx: MeasureContext) -> Response {
let mut size = (0., 0.);
if matches!(self.size.width, Size::Auto) || matches!(self.size.height, Size::Auto) {
//TODO optimized measure if only one of the sizes is auto
let res = ctx.text_measure.measure(self.font(ctx.current_font), self.text_size, &self.text);
size.0 = res.max_width;
size.1 = res.height;
}
Response {
size: compute_size(ctx.layout, self.size, size.into()),
..Default::default()
}
}
fn process(&self, ctx: ProcessContext) {
if self.text.is_empty() || self.color.w == 0. {
return
}
ctx.draw.add(UiDrawCommand::Text {
text: self.text.clone(),
position: layout.position,
position: ctx.layout.position,
size: self.text_size,
color: self.color,
font: self.font
font: self.font(ctx.current_font),
});
}
}

View file

@ -0,0 +1,80 @@
//! wrapper that allows applying various transformations to an element, such as translation, rotation, or scaling
use glam::{Affine2, Vec2};
use crate::{
draw::UiDrawCommand, element::{MeasureContext, ProcessContext, UiElement}, measure::Response
};
pub struct Transformer {
pub transform: Affine2,
pub element: Box<dyn UiElement>,
}
/// Wrapper that allows applying various transformations to an element, such as translation, rotation, or scaling\
/// Use sparingly, as this is an experimental feature and may not work as expected\
impl Transformer {
pub fn new(element: Box<dyn UiElement>) -> Self {
Self {
transform: Affine2::IDENTITY,
element,
}
}
pub fn translate(mut self, v: impl Into<Vec2>) -> Self {
self.transform *= Affine2::from_translation(v.into());
self
}
pub fn scale(mut self, v: impl Into<Vec2>) -> Self {
self.transform *= Affine2::from_scale(v.into());
self
}
pub fn rotate(mut self, radians: f32) -> Self {
self.transform *= Affine2::from_angle(radians);
self
}
}
impl UiElement for Transformer {
fn name(&self) -> &'static str {
"transformer"
}
fn measure(&self, ctx: MeasureContext) -> Response {
self.element.measure(ctx)
}
fn process(&self, ctx: ProcessContext) {
ctx.draw.add(UiDrawCommand::PushTransform(self.transform));
//This is stupid:
self.element.process(ProcessContext {
measure: ctx.measure,
state: ctx.state,
layout: ctx.layout,
draw: ctx.draw,
text_measure: ctx.text_measure,
current_font: ctx.current_font,
images: ctx.images,
input: ctx.input,
signal: ctx.signal,
});
ctx.draw.add(UiDrawCommand::PopTransform);
}
}
/// Extension trait for [`UiElement`] that adds the [`transform`] method
pub trait ElementTransformExt {
/// Wrap the element in a [`Transformer`]
///
/// This allows you to apply various transformations to the element, such as translation, rotation, or scaling\
/// Use sparingly, as this is an experimental feature and may not work as expected\
/// Transform is applied around the center of the element's bounding box.
fn transform(self) -> Transformer;
}
impl<T: UiElement + 'static> ElementTransformExt for T {
fn transform(self) -> Transformer {
Transformer::new(Box::new(self))
}
}

View file

@ -1,10 +1,37 @@
use glam::Vec2;
//! input, window events and event handling
use glam::Vec2;
use crate::input::{MouseButton, ButtonState, KeyboardKey};
#[derive(Clone, Copy, Debug)]
pub enum UiEvent {
MouseMove(Vec2),
MouseDown(Vec2),
MouseUp(Vec2),
KeyDown(u32),
KeyUp(u32),
MouseButton {
button: MouseButton,
state: ButtonState,
},
KeyboardButton {
key: KeyboardKey,
state: ButtonState,
},
TextInput(char),
}
#[derive(Default)]
pub(crate) struct EventQueue {
events: Vec<UiEvent>,
}
impl EventQueue {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn push(&mut self, event: UiEvent) {
self.events.push(event);
}
pub(crate) fn drain(&mut self) -> std::vec::Drain<UiEvent> {
self.events.drain(..)
}
}

27
hui/src/frame.rs Normal file
View file

@ -0,0 +1,27 @@
//! modular procedural background system
use crate::{draw::UiDrawCommandList, rect::Rect};
pub mod point;
mod rect;
pub mod stack;
pub mod nine_patch;
mod impls;
pub use rect::RectFrame;
/// Trait for a drawable frame
pub trait Frame {
/// Draw the frame at the given rect's position and size
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect);
/// Check if the frame is guaranteed to be fully opaque and fully cover the parent frame regardless of it's size
///
/// Returns true if the frame:
/// - Is fully opaque (i.e. `alpha >= 1.0`)
/// - Completely covers (or exceeds the size of) the frame
///
/// False negatives are acceptable, but false positives ***are not***.\
/// May be used for optimization purposes
fn covers_opaque(&self) -> bool { false }
}

180
hui/src/frame/impls.rs Normal file
View file

@ -0,0 +1,180 @@
use glam::{Vec3, Vec4};
use super::Frame;
use crate::{
color,
draw::{ImageHandle, UiDrawCommand, UiDrawCommandList},
rect::{Rect, Corners, FillColor},
};
impl Frame for ImageHandle {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
draw.add(UiDrawCommand::Rectangle {
position: rect.position,
size: rect.size,
color: color::WHITE.into(),
texture: Some(*self),
texture_uv: None,
rounded_corners: None,
})
}
fn covers_opaque(&self) -> bool {
false
}
}
impl Frame for FillColor {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
if self.is_transparent() {
return
}
draw.add(UiDrawCommand::Rectangle {
position: rect.position,
size: rect.size,
color: self.corners(),
texture: None,
texture_uv: None,
rounded_corners: None,
})
}
fn covers_opaque(&self) -> bool {
self.is_opaque()
}
}
// impl for various types resembling colors
// Corners (RGBA):
impl Frame for Corners<Vec4> {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
}
impl Frame for (Vec4, Vec4, Vec4, Vec4) {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
}
impl Frame for ((f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32)) {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
}
impl Frame for [[f32; 4]; 4] {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
}
// Corners (RGB):
impl Frame for Corners<Vec3> {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
true
}
}
impl Frame for (Vec3, Vec3, Vec3, Vec3) {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
true
}
}
impl Frame for ((f32, f32, f32), (f32, f32, f32), (f32, f32, f32), (f32, f32, f32)) {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
true
}
}
impl Frame for [[f32; 3]; 4] {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
true
}
}
// RGBA:
impl Frame for Vec4 {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
}
impl Frame for (f32, f32, f32, f32) {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
}
impl Frame for [f32; 4] {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
}
// RGB:
impl Frame for Vec3 {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
true
}
}
impl Frame for (f32, f32, f32) {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
true
}
}
impl Frame for [f32; 3] {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
true
}
}

238
hui/src/frame/nine_patch.rs Normal file
View file

@ -0,0 +1,238 @@
//! nine-patch frame implementation
//!
//! A 9-patch image is an image that can be scaled in a way that preserves the corners and edges of the image while scaling the center.
//! This is useful for creating scalable UI elements like buttons, windows, etc.
use glam::{vec2, UVec2, Vec2};
use crate::{
color,
draw::{ImageHandle, UiDrawCommand, UiDrawCommandList},
rect::{Rect, Corners, FillColor}
};
use super::Frame;
/// Represents a 9-patch image asset
#[derive(Clone, Copy, Debug)]
pub struct NinePatchAsset {
pub image: ImageHandle,
//TODO: remove this:
pub size: (u32, u32),
pub scalable_region: Rect,
}
//TODO allow scaling/moving corners
/// A 9-patch frame
///
/// Can optionally be tinted with a color (works well with grayscale assets)
#[derive(Clone, Copy, Debug)]
pub struct NinePatchFrame {
pub asset: NinePatchAsset,
pub color: FillColor,
}
impl NinePatchFrame {
pub fn from_asset(asset: NinePatchAsset) -> Self {
Self { asset, ..Default::default() }
}
pub fn with_color(mut self, color: impl Into<FillColor>) -> Self {
self.color = color.into();
self
}
}
impl Default for NinePatchFrame {
fn default() -> Self {
Self {
//This is not supposed to be left out as the default, so just set it to whatever :p
asset: NinePatchAsset { image: ImageHandle::default(), size: (0, 0), scalable_region: Rect::default() },
color: color::WHITE.into(),
}
}
}
impl Frame for NinePatchFrame {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
// without this, shїt gets messed up when the position is not a whole number
//XXX: should we round the size as well?
let position = rect.position.round();
let img_sz = UVec2::from(self.asset.size).as_vec2();
//Color stuff
let interpolate_color_rect = |uvs: Corners<Vec2>| {
Corners {
top_left: self.color.interpolate(uvs.top_left),
top_right: self.color.interpolate(uvs.top_right),
bottom_left: self.color.interpolate(uvs.bottom_left),
bottom_right: self.color.interpolate(uvs.bottom_right),
}
};
// Inset coords, in UV space
let region_uv = self.asset.scalable_region.corners();
// Inset coords, in image (px) space
let corners_image_px = Corners {
top_left: img_sz * region_uv.top_left,
top_right: img_sz * region_uv.top_right,
bottom_left: img_sz * region_uv.bottom_left,
bottom_right: img_sz * region_uv.bottom_right,
};
let size_h = (
corners_image_px.top_left.x,
rect.size.x - corners_image_px.top_left.x - (img_sz.x - corners_image_px.top_right.x),
img_sz.x - corners_image_px.top_right.x,
);
let size_v = (
corners_image_px.top_left.y,
rect.size.y - corners_image_px.top_left.y - (img_sz.y - corners_image_px.bottom_left.y),
img_sz.y - corners_image_px.bottom_left.y,
);
//Top-left patch
let top_left_patch_uv = Corners {
top_left: vec2(0., 0.),
top_right: vec2(region_uv.top_left.x, 0.),
bottom_left: vec2(0., region_uv.top_left.y),
bottom_right: region_uv.top_left,
};
draw.add(UiDrawCommand::Rectangle {
position,
size: vec2(size_h.0, size_v.0),
color: interpolate_color_rect(top_left_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(top_left_patch_uv),
rounded_corners: None
});
//Top patch
let top_patch_uv = Corners {
top_left: vec2(region_uv.top_left.x, 0.),
top_right: vec2(region_uv.top_right.x, 0.),
bottom_left: region_uv.top_left,
bottom_right: region_uv.top_right,
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0, 0.),
size: vec2(size_h.1, size_v.0),
color: interpolate_color_rect(top_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(top_patch_uv),
rounded_corners: None
});
//Top-right patch
let top_right_patch_uv = Corners {
top_left: vec2(region_uv.top_right.x, 0.),
top_right: vec2(1., 0.),
bottom_left: region_uv.top_right,
bottom_right: vec2(1., region_uv.top_right.y),
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0 + size_h.1, 0.),
size: vec2(size_h.2, size_v.0),
color: interpolate_color_rect(top_right_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(top_right_patch_uv),
rounded_corners: None
});
//Left patch
let left_patch_uv = Corners {
top_left: vec2(0., region_uv.top_left.y),
top_right: region_uv.top_left,
bottom_left: vec2(0., region_uv.bottom_left.y),
bottom_right: region_uv.bottom_left,
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(0., size_v.0),
size: vec2(size_h.0, size_v.1),
color: interpolate_color_rect(left_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(left_patch_uv),
rounded_corners: None
});
// Center patch
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0, size_v.0),
size: vec2(size_h.1, size_v.1),
color: interpolate_color_rect(region_uv),
texture: Some(self.asset.image),
texture_uv: Some(region_uv),
rounded_corners: None
});
//Right patch
let right_patch_uv = Corners {
top_left: region_uv.top_right,
top_right: vec2(1., region_uv.top_right.y),
bottom_left: region_uv.bottom_right,
bottom_right: vec2(1., region_uv.bottom_right.y),
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0 + size_h.1, size_v.0),
size: vec2(size_h.2, size_v.1),
color: interpolate_color_rect(right_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(right_patch_uv),
rounded_corners: None
});
//Bottom-left patch
let bottom_left_patch_uv = Corners {
top_left: vec2(0., region_uv.bottom_left.y),
top_right: region_uv.bottom_left,
bottom_left: vec2(0., 1.),
bottom_right: vec2(region_uv.bottom_left.x, 1.),
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(0., size_v.0 + size_v.1),
size: vec2(size_h.0, size_v.2),
color: interpolate_color_rect(bottom_left_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(bottom_left_patch_uv),
rounded_corners: None
});
//Bottom patch
let bottom_patch_uv = Corners {
top_left: region_uv.bottom_left,
top_right: region_uv.bottom_right,
bottom_left: vec2(region_uv.bottom_left.x, 1.),
bottom_right: vec2(region_uv.bottom_right.x, 1.),
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0, size_v.0 + size_v.1),
size: vec2(size_h.1, size_v.2),
color: interpolate_color_rect(bottom_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(bottom_patch_uv),
rounded_corners: None
});
//Bottom-right patch
let bottom_right_patch_uv = Corners {
top_left: region_uv.bottom_right,
top_right: vec2(1., region_uv.bottom_right.y),
bottom_left: vec2(region_uv.bottom_right.x, 1.),
bottom_right: vec2(1., 1.),
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0 + size_h.1, size_v.0 + size_v.1),
size: vec2(size_h.2, size_v.2),
color: interpolate_color_rect(bottom_right_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(bottom_right_patch_uv),
rounded_corners: None
});
}
fn covers_opaque(&self) -> bool {
false
}
}

202
hui/src/frame/point.rs Normal file
View file

@ -0,0 +1,202 @@
//! frame-relative positioning/size
use glam::{Vec2, vec2};
use derive_more::{Add, AddAssign, Sub, SubAssign};
use crate::layout::{Size, Size2d};
/// Point inside a frame
///
/// Can be absolute, relative, or a combination of both\
/// Final coordinate is calculated as `absolute + relative * parent_size`
#[derive(Clone, Copy, Debug, Default, Add, AddAssign, Sub, SubAssign)]
pub struct FramePoint {
/// Absolute positioning
pub absolute: f32,
/// Relative positioning
pub relative: f32,
}
impl From<f32> for FramePoint {
fn from(value: f32) -> Self {
Self::absolute(value)
}
}
impl From<Size> for FramePoint {
/// Convert a `Size` into a `FramePoint`
///
/// This function behaves just as you would expect, but:
/// - `Auto` is always treated as `BEGIN`
/// - `Remaining` is treated as `Relative`
fn from(size: Size) -> Self {
match size {
Size::Auto => Self::BEGIN,
Size::Relative(value) | Size::Remaining(value) => Self::relative(value),
Size::Absolute(value) => Self::absolute(value),
}
}
}
impl FramePoint {
/// Beginning of the frame axis
pub const BEGIN: Self = Self {
absolute: 0.0,
relative: 0.0,
};
/// Center of the frame axis
pub const CENTER: Self = Self {
absolute: 0.0,
relative: 0.5,
};
/// End of the frame axis
pub const END: Self = Self {
absolute: 0.0,
relative: 1.0,
};
/// Create a new absolutely positioned `FramePoint`
pub const fn absolute(value: f32) -> Self {
Self {
absolute: value,
relative: 0.0,
}
}
/// Create a new relatively positioned `FramePoint`
pub const fn relative(value: f32) -> Self {
Self {
absolute: 0.0,
relative: value,
}
}
/// Create a new `FramePoint` with both absolute and relative positioning
pub const fn relative_absolute(relative: f32, absolute: f32) -> Self {
Self {
absolute,
relative,
}
}
/// Resolve the `FramePoint` into an absolute coordinate
pub(crate) fn resolve(&self, parent_size: f32) -> f32 {
self.absolute + self.relative * parent_size
}
}
/// A 2-dimensional [`FramePoint`]
#[derive(Clone, Copy, Debug, Default, Add, AddAssign, Sub, SubAssign)]
pub struct FramePoint2d {
pub x: FramePoint,
pub y: FramePoint,
}
impl From<(FramePoint, FramePoint)> for FramePoint2d {
fn from((x, y): (FramePoint, FramePoint)) -> Self {
Self { x, y }
}
}
impl From<Size> for FramePoint2d {
fn from(size: Size) -> Self {
Self {
x: size.into(),
y: size.into(),
}
}
}
impl From<Size2d> for FramePoint2d {
fn from(size: Size2d) -> Self {
Self {
x: size.width.into(),
y: size.height.into(),
}
}
}
impl From<(f32, f32)> for FramePoint2d {
fn from((x, y): (f32, f32)) -> Self {
Self {
x: FramePoint::absolute(x),
y: FramePoint::absolute(y),
}
}
}
impl From<Vec2> for FramePoint2d {
fn from(vec: Vec2) -> Self {
Self {
x: FramePoint::absolute(vec.x),
y: FramePoint::absolute(vec.y),
}
}
}
impl FramePoint2d {
/// Top left corner of the frame
pub const TOP_LEFT: Self = Self {
x: FramePoint::BEGIN,
y: FramePoint::BEGIN,
};
/// Top center of the frame
pub const TOP_CENTER: Self = Self {
x: FramePoint::CENTER,
y: FramePoint::BEGIN,
};
/// Top right corner of the frame
pub const TOP_RIGHT: Self = Self {
x: FramePoint::END,
y: FramePoint::BEGIN,
};
/// Center left of the frame
pub const CENTER_LEFT: Self = Self {
x: FramePoint::BEGIN,
y: FramePoint::CENTER,
};
/// Center of the frame
pub const CENTER: Self = Self {
x: FramePoint::CENTER,
y: FramePoint::CENTER,
};
/// Center right of the frame
pub const CENTER_RIGHT: Self = Self {
x: FramePoint::END,
y: FramePoint::CENTER,
};
/// Bottom left corner of the frame
pub const BOTTOM_LEFT: Self = Self {
x: FramePoint::BEGIN,
y: FramePoint::END,
};
/// Bottom center of the frame
pub const BOTTOM_CENTER: Self = Self {
x: FramePoint::CENTER,
y: FramePoint::END,
};
/// Bottom right corner of the frame
pub const BOTTOM_RIGHT: Self = Self {
x: FramePoint::END,
y: FramePoint::END,
};
/// Resolve the `FramePoint2d` into an absolute coordinate within the frame's coordinate system\
///
/// (point with absolute cordinates, relative to the frame's top-left corner)
pub(crate) fn resolve(&self, parent_size: Vec2) -> Vec2 {
let x = self.x.resolve(parent_size.x);
let y = self.y.resolve(parent_size.y);
vec2(x, y)
}
}

150
hui/src/frame/rect.rs Normal file
View file

@ -0,0 +1,150 @@
use glam::Vec2;
use crate::{
color,
draw::{ImageHandle, RoundedCorners, UiDrawCommand, UiDrawCommandList},
rect::{Rect, Corners, FillColor},
};
use super::{Frame, point::FramePoint2d};
/// A rectangular frame
///
/// Can optionally be tinted, textured, and have rounded corners
#[derive(Clone, Copy)]
pub struct RectFrame {
/// Background color of the frame\
///
/// If the container has a background texture, it will be multiplied by this color
pub color: FillColor,
/// Background texture of the frame
///
/// Can be used in conjunction with the background color\
/// In this case, the texture will be shaded by the color
///
/// Please note that if the background color is NOT set (or set to transparent), the texture will NOT be visible\
/// This is because the texture is multiplied by the color, and if the color is transparent, the texture will be too\
pub image: Option<ImageHandle>,
/// Top left corner of the rectangle
pub top_left: FramePoint2d,
/// Bottom right corner of the rectangle
pub bottom_right: FramePoint2d,
/// Corner radius of the frame
pub corner_radius: Corners<f32>,
}
// impl<T: Into<FillColor>> From<T> for RectFrame {
// fn from(color: T) -> Self {
// Self::from_color(color)
// }
// }
impl From<FillColor> for RectFrame {
fn from(color: FillColor) -> Self {
Self::color(color)
}
}
impl From<ImageHandle> for RectFrame {
fn from(image: ImageHandle) -> Self {
Self::image(image)
}
}
impl RectFrame {
/// Create a new [`RectFrame`] with the given color
pub fn color(color: impl Into<FillColor>) -> Self {
Self {
color: color.into(),
..Self::default()
}
}
/// Create a new [`RectFrame`] with the given image\
///
/// Color will be set to [`WHITE`](crate::color::WHITE) to ensure the image is visible
pub fn image(image: ImageHandle) -> Self {
Self {
color: color::WHITE.into(),
image: Some(image),
..Self::default()
}
}
/// Create a new [`RectFrame`] with the given color and image
pub fn color_image(color: impl Into<FillColor>, image: ImageHandle) -> Self {
Self {
color: color.into(),
image: Some(image),
..Self::default()
}
}
/// Set the corner radius of the [`RectFrame`]
pub fn with_corner_radius(self, radius: impl Into<Corners<f32>>) -> Self {
Self {
corner_radius: radius.into(),
..self
}
}
//TODO: deprecate and replace
/// Inset the rectangle by the given amount in pixels
pub fn with_inset(self, inset: f32) -> Self {
Self {
top_left: self.top_left + Vec2::splat(inset).into(),
bottom_right: self.bottom_right - Vec2::splat(inset).into(),
..self
}
}
}
impl Default for RectFrame {
fn default() -> Self {
Self {
color: FillColor::transparent(),
image: None,
top_left: FramePoint2d::TOP_LEFT,
bottom_right: FramePoint2d::BOTTOM_RIGHT,
corner_radius: Corners::all(0.),
}
}
}
impl Frame for RectFrame {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
if self.color.is_transparent() {
return
}
//TODO: handle bottom_right < top_left
let top_left = self.top_left.resolve(rect.size);
let bottom_right = self.bottom_right.resolve(rect.size);
draw.add(UiDrawCommand::Rectangle {
position: rect.position + top_left,
size: bottom_right - top_left,
color: self.color.corners(),
texture: self.image,
texture_uv: None,
rounded_corners: (self.corner_radius.max_f32() > 0.).then_some(
RoundedCorners::from_radius(self.corner_radius)
),
});
}
fn covers_opaque(&self) -> bool {
self.top_left.x.relative <= 0. &&
self.top_left.x.absolute <= 0. &&
self.top_left.y.relative <= 0. &&
self.top_left.y.absolute <= 0. &&
self.bottom_right.x.relative >= 1. &&
self.bottom_right.x.absolute >= 0. &&
self.bottom_right.y.relative >= 1. &&
self.bottom_right.y.absolute >= 0. &&
self.color.is_opaque() &&
self.image.is_none() &&
self.corner_radius.max_f32() == 0.
}
}

37
hui/src/frame/stack.rs Normal file
View file

@ -0,0 +1,37 @@
//! allows stacking two frames on top of each other
use crate::{draw::UiDrawCommandList, rect::Rect};
use super::Frame;
/// A frame that draws two frames on top of each other
pub struct FrameStack(pub Box<dyn Frame>, pub Box<dyn Frame>);
impl Frame for FrameStack {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
self.0.draw(draw, rect);
self.1.draw(draw, rect);
}
fn covers_opaque(&self) -> bool {
self.0.covers_opaque() ||
self.1.covers_opaque()
}
}
pub trait FrameStackExt: Frame {
/// Stack another frame on top of this one
fn stack(self, other: impl Frame + 'static) -> FrameStack;
/// Stack another frame below this one
fn stack_below(self, other: impl Frame + 'static) -> FrameStack;
}
impl<T: Frame + 'static> FrameStackExt for T {
fn stack(self, other: impl Frame + 'static) -> FrameStack {
FrameStack(Box::new(self), Box::new(other))
}
fn stack_below(self, other: impl Frame + 'static) -> FrameStack {
FrameStack(Box::new(other), Box::new(self))
}
}

337
hui/src/input.rs Normal file
View file

@ -0,0 +1,337 @@
//! keyboard, mouse, and touch input handling
use std::hash::{Hash, Hasher};
use glam::Vec2;
use hashbrown::HashMap;
use nohash_hasher::BuildNoHashHasher;
use tinyset::{Fits64, Set64};
use crate::{event::{EventQueue, UiEvent}, rect::Rect};
/// Represents a mouse button.
///
/// Value of the `Other` variant is currently not standardized\
/// and may change depending on the platform or the backend used
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum MouseButton {
///Primary mouse button (usually left)
#[default]
Primary,
///Secondary mouse button (usually right)
Secondary,
///Middle mouse button (usually the wheel button)
Middle,
///Other mouse button (e.g. extra buttons on a gaming mouse)
///
///Value is not standardized and may change depending on the platform or the backend used
Other(u8),
}
// Manual hash impl only uses one hash call
impl Hash for MouseButton {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
MouseButton::Primary => 0u16.hash(state),
MouseButton::Secondary => 1u16.hash(state),
MouseButton::Middle => 2u16.hash(state),
MouseButton::Other(id) => ((*id as u16) << 8).hash(state),
}
}
}
/// Represents the state of a button, such as a mouse button or a keyboard key.\
/// Can be either `Pressed` (0) or `Released` (1).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum ButtonState {
#[default]
Released = 0,
Pressed = 1,
}
impl From<bool> for ButtonState {
fn from(b: bool) -> Self {
if b { ButtonState::Pressed } else { ButtonState::Released }
}
}
impl From<ButtonState> for bool {
fn from(s: ButtonState) -> Self {
s.is_pressed()
}
}
impl ButtonState {
/// Returns `true` if the button is pressed
pub fn is_pressed(self) -> bool {
self == ButtonState::Pressed
}
/// Returns `true` if the button is released
pub fn is_released(self) -> bool {
self == ButtonState::Released
}
}
/// Represents a keyboard or other hardware key (for example volume buttons)
///
/// Values of the `KeyCode` variant are not standardized and may change depending on the platform or the backend used.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum KeyboardKey {
//Keyboard buttons:
A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14, P = 15, Q = 16, R = 17, S = 18, T = 19, U = 20, V = 21, W = 22, X = 23, Y = 24, Z = 25,
Num0 = 26, Num1 = 27, Num2 = 28, Num3 = 29, Num4 = 30, Num5 = 31, Num6 = 32, Num7 = 33, Num8 = 34, Num9 = 35,
Np0 = 36, Np1 = 37, Np2 = 38, Np3 = 39, Np4 = 40, Np5 = 41, Np6 = 42, Np7 = 43, Np8 = 44, Np9 = 45,
NpDivide = 46, NpMultiply = 47, NpSubtract = 48, NpAdd = 49, NpEnter = 50, NpDecimal = 51,
F1 = 52, F2 = 53, F3 = 54, F4 = 55, F5 = 56, F6 = 57, F7 = 58, F8 = 59, F9 = 60, F10 = 61, F11 = 62, F12 = 63,
Up = 64, Down = 65, Left = 66, Right = 67,
Space = 68, Enter = 69, Escape = 70, Backspace = 71, Tab = 72, CapsLock = 73,
LControl = 74, RControl = 75, LShift = 76, RShift = 77, LAlt = 78, RAlt = 79, LSuper = 80, RSuper = 81,
Grave = 82, Minus = 83, Equals = 84, LeftBracket = 85, RightBracket = 86, Backslash = 87, Semicolon = 88, Apostrophe = 89, Comma = 90, Period = 91, Slash = 92,
Insert = 93, Delete = 94, Home = 95, End = 96, PageUp = 97, PageDown = 98, PrintScreen = 99, ScrollLock = 100, Pause = 101, Menu = 102, NumLock = 103,
//Multimedia keys and android-specific (e.g. volume keys):
Mute = 104, VolumeUp = 105, VolumeDown = 106, MediaPlay = 107, MediaStop = 108, MediaNext = 109, MediaPrevious = 110,
//Keycode:
/// Represents a key code.
///
/// This enum variant holds an unsigned 32-bit integer representing a key code.\
/// The value of the key code is not standardized and may change depending on the platform or the backend used.
KeyCode(u32),
}
macro_rules! impl_fits64_for_keyboard_key {
($($i:ident = $v:literal),*) => {
impl Fits64 for KeyboardKey {
// SAFETY: not actually doing anything unsafe
#[allow(unsafe_code)]
unsafe fn from_u64(x: u64) -> Self {
match x {
$( $v => KeyboardKey::$i, )*
_ => KeyboardKey::KeyCode(x as u32),
}
}
fn to_u64(self) -> u64 {
match self {
$( KeyboardKey::$i => $v, )*
KeyboardKey::KeyCode(x) => x as u64 | 0x8000000000000000u64,
}
}
}
};
}
impl_fits64_for_keyboard_key!(
A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14, P = 15, Q = 16, R = 17, S = 18, T = 19, U = 20, V = 21, W = 22, X = 23, Y = 24, Z = 25,
Num0 = 26, Num1 = 27, Num2 = 28, Num3 = 29, Num4 = 30, Num5 = 31, Num6 = 32, Num7 = 33, Num8 = 34, Num9 = 35,
Np0 = 36, Np1 = 37, Np2 = 38, Np3 = 39, Np4 = 40, Np5 = 41, Np6 = 42, Np7 = 43, Np8 = 44, Np9 = 45,
NpDivide = 46, NpMultiply = 47, NpSubtract = 48, NpAdd = 49, NpEnter = 50, NpDecimal = 51,
F1 = 52, F2 = 53, F3 = 54, F4 = 55, F5 = 56, F6 = 57, F7 = 58, F8 = 59, F9 = 60, F10 = 61, F11 = 62, F12 = 63,
Up = 64, Down = 65, Left = 66, Right = 67,
Space = 68, Enter = 69, Escape = 70, Backspace = 71, Tab = 72, CapsLock = 73,
LControl = 74, RControl = 75, LShift = 76, RShift = 77, LAlt = 78, RAlt = 79, LSuper = 80, RSuper = 81,
Grave = 82, Minus = 83, Equals = 84, LeftBracket = 85, RightBracket = 86, Backslash = 87, Semicolon = 88, Apostrophe = 89, Comma = 90, Period = 91, Slash = 92,
Insert = 93, Delete = 94, Home = 95, End = 96, PageUp = 97, PageDown = 98, PrintScreen = 99, ScrollLock = 100, Pause = 101, Menu = 102, NumLock = 103,
Mute = 104, VolumeUp = 105, VolumeDown = 106, MediaPlay = 107, MediaStop = 108, MediaNext = 109, MediaPrevious = 110
);
/// Information about the current state of a pressed mouse button
#[derive(Default, Clone, Copy, Debug)]
pub struct MouseButtonMeta {
/// Position at which the input was initiated (last time it was pressed **down**)
pub start_position: Vec2,
}
#[derive(Default)]
pub struct MouseState {
/// Current position of the mouse pointer
pub current_position: Vec2,
/// Position of the mouse pointer on the previous frame
pub prev_position: Vec2,
/// Current state of each mouse button (if down)
pub buttons: HashMap<MouseButton, MouseButtonMeta, BuildNoHashHasher<u16>>,
/// mouse buttons that were released *in the current frame*
pub released_buttons: HashMap<MouseButton, MouseButtonMeta, BuildNoHashHasher<u16>>,
}
/// Unique identifier of a touch pointer (finger)
pub type TouchId = u32;
pub struct TouchFinger {
/// Unique identifier of the pointer/finger
pub id: TouchId,
/// Current position of the pointer/finger
pub current_position: Vec2,
pub start_position: Vec2,
}
pub(crate) struct UiInputState {
// pointers: HashMap<u32, Pointer, BuildNoHashHasher<u32>>,
mouse_pointer: MouseState,
keyboard_state: Set64<KeyboardKey>,
/// events that happened in the current frame
just_happened: Vec<UiEvent>,
}
impl UiInputState {
pub fn new() -> Self {
Self {
// pointers: HashMap::default(),
mouse_pointer: MouseState::default(),
keyboard_state: Set64::new(),
just_happened: Vec::new(),
}
}
/// Drain the event queue and update the internal input state
///
/// This function should be called exactly once per frame
pub fn update_state(&mut self, event_queue: &mut EventQueue) {
self.mouse_pointer.prev_position = self.mouse_pointer.current_position;
self.mouse_pointer.released_buttons.clear();
self.just_happened.clear();
self.just_happened.extend(event_queue.drain());
for event in &self.just_happened {
#[allow(clippy::single_match)]
match event {
UiEvent::MouseMove(pos) => {
self.mouse_pointer.current_position = *pos;
},
UiEvent::MouseButton { button, state } => {
match state {
//wtf should we do with buttons that are pressed and released in the same frame?
//i have no fvcking idea
ButtonState::Pressed => {
let button = self.mouse_pointer.buttons.entry(*button)
.or_insert(MouseButtonMeta::default());
button.start_position = self.mouse_pointer.current_position;
},
ButtonState::Released => {
//log::trace!("Button {:?} was released", button);
if let Some(button_meta) = self.mouse_pointer.buttons.remove(button) {
//log::trace!("start pos was: {:?} current pos is: {:?}", button_meta.start_position, self.mouse_pointer.current_position);
self.mouse_pointer.released_buttons.insert(*button, button_meta);
} else {
//huh
//this can happen i guess ¯\_(=^・ω・^)_/¯
self.mouse_pointer.released_buttons.insert(*button, MouseButtonMeta {
start_position: self.mouse_pointer.current_position,
});
}
},
}
},
UiEvent::KeyboardButton { key, state } => {
match state {
ButtonState::Pressed => self.keyboard_state.insert(*key),
ButtonState::Released => self.keyboard_state.remove(key),
};
},
//TODO touch, text input
_ => (),
}
}
}
pub fn ctx(&self) -> InputCtx {
InputCtx(self)
}
}
/// Response for checks that involve an active pointer
#[derive(Clone, Copy, Debug)]
pub struct ActiveCheckResponse {
/// Current position of the pointer inside the target rectangle's coordinate space
pub position_in_rect: Vec2,
/// Position of the pointer at the time the start of the input inside the target rectangle's coordinate space
pub start_position_in_rect: Vec2,
/// Position of the pointer on the previous frame inside the target rectangle's coordinate space
pub last_position_in_rect: Vec2,
}
#[derive(Clone, Copy)]
pub struct InputCtx<'a>(&'a UiInputState);
impl<'a> InputCtx<'a> {
/// Get the current position of the mouse pointer
///
/// Do not use this function to check for hover, use [`InputCtx::check_hover`] instead
pub fn mouse_position(&self) -> Vec2 {
self.0.mouse_pointer.current_position
}
/// Get the current position of the mouse pointer within a rectangle
///
/// Do not use this function to check for hover, use [`InputCtx::check_hover`] instead
pub fn mouse_position_in_rect(&self, rect: Rect) -> Option<Vec2> {
let pos = self.0.mouse_pointer.current_position;
rect.contains_point(pos).then_some(pos - rect.position)
}
/// Get the state of a mouse button
pub fn mouse_button_down(&self, button: MouseButton) -> ButtonState {
self.0.mouse_pointer.buttons.contains_key(&button).into()
}
/// Get the start position of a mouse button\
/// (Position at the last time it was pressed **down**)
///
/// Returns `None` if the button is not currently down
pub fn mouse_button_start_position(&self, button: MouseButton) -> Option<Vec2> {
self.0.mouse_pointer.buttons.get(&button).map(|meta| meta.start_position)
}
/// Get the relative movement of the mouse pointer since the button was pressed down
///
/// This function is similar to [`InputCtx::mouse_button_start_position`], but returns the relative movement instead of the absolute position
pub fn mouse_button_relative_movement(&self, button: MouseButton) -> Option<Vec2> {
let start = self.mouse_button_start_position(button)?;
Some(self.mouse_position() - start)
}
/// Check if a rect can be considered "hovered"
///
/// This can be triggered by multiple input sources, such as mouse, touch, etc.
pub fn check_hover(&self, rect: Rect) -> bool {
rect.contains_point(self.0.mouse_pointer.current_position)
}
/// Check if a rect can be considered "clicked" in the current frame
///
/// This can be triggered by multiple input sources, such as mouse, touch, etc.\
/// In case of a mouse, these conditions must be met:
/// - The mouse button got released in the current frame
/// - The mouse pointer is currently inside the rectangle
/// - The mouse pointer was inside the rectangle at the time the button was pressed down
///
/// By default, this function only checks for the primary mouse button\
/// This is a limitation of the current API and may change in the future\
/// (as the current implementation of this function checks for both mouse and touch input, and the touch input quite obviously only supports one "button")
pub fn check_click(&self, rect: Rect) -> Option<ActiveCheckResponse> {
let pos = self.0.mouse_pointer.current_position;
self.0.mouse_pointer.released_buttons.get(&MouseButton::Primary).filter(|meta| {
rect.contains_point(meta.start_position) && rect.contains_point(pos)
}).map(|mi| ActiveCheckResponse {
position_in_rect: pos - rect.position,
start_position_in_rect: mi.start_position - rect.position,
last_position_in_rect: self.0.mouse_pointer.prev_position - rect.position,
})
}
// TODO: write better docs
/// Check if a rect is being actively being interacted with (e.g. dragged)
pub fn check_active(&self, rect: Rect) -> Option<ActiveCheckResponse> {
self.0.mouse_pointer.buttons.get(&MouseButton::Primary).filter(|mi| {
rect.contains_point(mi.start_position)
}).map(|mi| ActiveCheckResponse {
position_in_rect: self.0.mouse_pointer.current_position - rect.position,
start_position_in_rect: mi.start_position - rect.position,
last_position_in_rect: self.0.mouse_pointer.prev_position - rect.position,
})
}
}

308
hui/src/instance.rs Normal file
View file

@ -0,0 +1,308 @@
use glam::Vec2;
use crate::{
element::{MeasureContext, ProcessContext, UiElement},
layout::{Direction, LayoutInfo},
text::{FontHandle, TextRenderer},
draw::{
ImageHandle,
TextureFormat,
UiDrawCall,
UiDrawCommandList,
atlas::{TextureAtlasManager, TextureAtlasMeta},
},
signal::{Signal, SignalStore},
event::{EventQueue, UiEvent},
input::UiInputState,
rect::Rect,
state::StateRepo,
};
/// The main instance of the UI system.
///
/// In most cases, you should only have one instance of this struct, but multiple instances are allowed\
/// (Please note that it's possible to render multiple UI "roots" using a single instance)
pub struct UiInstance {
stateful_state: StateRepo,
prev_draw_commands: UiDrawCommandList,
draw_commands: UiDrawCommandList,
draw_call: UiDrawCall,
draw_call_modified: bool,
text_renderer: TextRenderer,
atlas: TextureAtlasManager,
events: EventQueue,
input: UiInputState,
signal: SignalStore,
/// True if in the middle of a laying out a frame
state: bool,
}
impl UiInstance {
/// Crate and initialize a new instance of the UI
///
/// In most cases, you should only do this *once*, during the initialization of your application
pub fn new() -> Self {
UiInstance {
//mouse_position: Vec2::ZERO,
stateful_state: StateRepo::new(),
//event_queue: VecDeque::new(),
// root_elements: Vec::new(),
prev_draw_commands: UiDrawCommandList::default(),
draw_commands: UiDrawCommandList::default(),
draw_call: UiDrawCall::default(),
draw_call_modified: false,
// ftm: FontTextureManager::default(),
text_renderer: TextRenderer::new(),
atlas: {
let mut atlas = TextureAtlasManager::default();
atlas.add_dummy();
atlas
},
events: EventQueue::new(),
input: UiInputState::new(),
signal: SignalStore::new(),
state: false,
}
}
/// Parse and add a font from a raw byte slice to the UI\
/// TrueType (`.ttf`/`.ttc`) and OpenType (`.otf`) fonts are supported\
///
/// Returns a font handle ([`FontHandle`]).
///
/// ## Panics:
/// If the font data is invalid or corrupt
pub fn add_font(&mut self, font: &[u8]) -> FontHandle {
self.text_renderer.add_font_from_bytes(font)
}
/// Add an image to the texture atlas\
/// Accepted texture formats are `Rgba` and `Grayscale`
///
/// Returns an image handle ([`ImageHandle`])\
/// This handle can be used to reference the texture in draw commands\
/// It's a light reference and can be cloned/copied freely, but will not be cleaned up even when dropped
pub fn add_image(&mut self, format: TextureFormat, data: &[u8], width: usize) -> ImageHandle {
self.atlas.add(width, data, format)
}
//TODO better error handling
/// Add an image from a file to the texture atlas\
/// (experimental, may be removed in the future)
///
/// Requires the `image` feature
///
/// # Panics:
/// - If the file exists but contains invalid image data\
/// (this will change to a soft error in the future)
#[cfg(feature = "image")]
pub fn add_image_file_path(&mut self, path: impl AsRef<std::path::Path>) -> Result<ImageHandle, std::io::Error> {
use std::io::{Read, Seek};
// Open the file (and wrap it in a bufreader)
let mut file = std::io::BufReader::new(std::fs::File::open(path)?);
//Guess the image format from the magic bytes
//Read like 64 bytes, which should be enough for magic byte detection
//well this would fail if the image is somehow smaller than 64 bytes, but who the fvck cares...
let mut magic = [0; 64];
file.read_exact(&mut magic)?;
let format = image::guess_format(&magic).expect("Invalid image data (FORMAT)");
file.seek(std::io::SeekFrom::Start(0))?;
//Parse the image and read the raw uncompressed rgba data
let image = image::load(file, format).expect("Invalid image data");
let image_rgba = image.as_rgba8().unwrap();
//Add the image to the atlas
let handle = self.add_image(
TextureFormat::Rgba,
image_rgba,
image.width() as usize
);
Ok(handle)
}
/// Push a font to the font stack\
/// The font will be used for all text rendering until it is popped
///
/// This function is useful for replacing the default font, use sparingly\
/// (This library attempts to be stateless, however passing the font to every text element is not very practical)
pub fn push_font(&mut self, font: FontHandle) {
self.text_renderer.push_font(font);
}
/// Pop a font from the font stack\
///
/// ## Panics:
/// If the font stack is empty
pub fn pop_font(&mut self) {
self.text_renderer.pop_font();
}
/// Get the current default font
pub fn current_font(&self) -> FontHandle {
self.text_renderer.current_font()
}
/// Add an element or an element tree to the UI
///
/// Use the `rect` parameter to specify the position and size of the element\
/// (usually, the size of the window/screen)
///
/// ## Panics:
/// If called while the UI is not active (call [`UiInstance::begin`] first)
pub fn add(&mut self, element: impl UiElement, rect: impl Into<Rect>) {
assert!(self.state, "must call UiInstance::begin before adding elements");
let rect: Rect = rect.into();
let layout = LayoutInfo {
position: rect.position,
max_size: rect.size,
direction: Direction::Vertical,
remaining_space: None,
};
let measure = element.measure(MeasureContext {
state: &self.stateful_state,
layout: &layout,
text_measure: self.text_renderer.to_measure(),
current_font: self.text_renderer.current_font(),
images: self.atlas.context(),
});
element.process(ProcessContext {
measure: &measure,
state: &mut self.stateful_state,
layout: &layout,
draw: &mut self.draw_commands,
text_measure: self.text_renderer.to_measure(),
current_font: self.text_renderer.current_font(),
images: self.atlas.context(),
input: self.input.ctx(),
signal: &mut self.signal,
});
}
/// Prepare the UI for layout and processing\
/// You must call this function at the beginning of the frame, before adding any elements\
///
/// ## Panics:
/// If called twice in a row (for example, if you forget to call [`UiInstance::end`])\
/// This is an indication of a bug in your code and should be fixed.
pub fn begin(&mut self) {
//check and update current state
assert!(!self.state, "must call UiInstance::end before calling UiInstance::begin again");
self.state = true;
//first, drain and process the event queue
self.input.update_state(&mut self.events);
//then, reset the (remaining) signals
self.signal.clear();
//then, reset the draw commands
std::mem::swap(&mut self.prev_draw_commands, &mut self.draw_commands);
self.draw_commands.commands.clear();
self.draw_call_modified = false;
//reset atlas modification flag
self.atlas.reset_modified();
}
/// End the frame and prepare the UI for rendering\
/// You must call this function at the end of the frame, before rendering the UI
///
/// ## Panics:
/// If called without calling [`UiInstance::begin`] first. (or if called twice)\
/// This is an indication of a bug in your code and should be fixed.
pub fn end(&mut self) {
//check and update current state
assert!(self.state, "must call UiInstance::begin before calling UiInstance::end");
self.state = false;
//check if the draw commands have been modified
if self.draw_commands.commands == self.prev_draw_commands.commands {
return
}
//if they have, rebuild the draw call and set the modified flag
self.draw_call = UiDrawCall::build(&self.draw_commands, &mut self.atlas, &mut self.text_renderer);
self.draw_call_modified = true;
}
/// Get the draw call information for the current frame
///
/// This function should only be used by the render backend.\
/// You should not call this directly unless you're implementing a custom render backend
///
/// Returns a tuple with a boolean indicating if the buffers have been modified since the last frame
///
/// You should only call this function *after* [`UiInstance::end`]\
/// Calling it in the middle of a frame will result in a warning but will not cause a panic\
/// (please note that doing so is probably a mistake and should be fixed in your code)\
/// Doing so anyway will return draw call data for the previous frame, but the `modified` flag will *always* be incorrect until [`UiInstance::end`] is called
///
pub fn draw_call(&self) -> (bool, &UiDrawCall) {
if self.state {
log::warn!("UiInstance::draw_call called while in the middle of a frame, this is probably a mistake");
}
(self.draw_call_modified, &self.draw_call)
}
/// Get the texture atlas size and data for the current frame
///
/// This function should only be used by the render backend.\
/// You should not call this directly unless you're implementing a custom render backend
///
/// You should only call this function *after* [`UiInstance::end`]\
/// Calling it in the middle of a frame will result in a warning but will not cause a panic\
/// (please note that doing so is probably a mistake and should be fixed in your code)\
/// Using this function in the middle of a frame will return partially modified atlas data that may be outdated or incomplete\
/// This will lead to rendering artifacts, 1-frame delays and flashes and is probably not what you want
///
/// Make sure to check [`TextureAtlasMeta::modified`] to see if the texture has been modified
/// since the beginning of the current frame before uploading it to the GPU
pub fn atlas(&self) -> TextureAtlasMeta {
if self.state {
log::warn!("UiInstance::atlas called while in the middle of a frame, this is probably a mistake");
}
self.atlas.meta()
}
/// Push a platform event to the UI event queue
///
/// You should call this function *before* calling [`UiInstance::begin`] or after calling [`UiInstance::end`]\
/// Calling it in the middle of a frame will result in a warning but will not cause a panic\
/// (please note that doing so is probably a mistake and should be fixed in your code)\
/// In this case, the event will be processed in the next frame, but in some cases may affect the current frame.\
/// (The exact behavior is not guaranteed and you should avoid doing this if possible)
///
/// This function should only be used by the platform backend.\
/// You should not call this directly unless you're implementing a custom platform backend
/// or have a very specific usecase
pub fn push_event(&mut self, event: UiEvent) {
if self.state {
log::warn!("UiInstance::push_event called while in the middle of a frame, this is probably a mistake");
}
self.events.push(event);
}
/// Push a "fake" signal to the UI signal queue
pub fn push_signal<T: Signal + 'static>(&mut self, signal: T) {
self.signal.add(signal);
}
//TODO: offer a non-consuming version of this function for T: Clone
/// Process all signals of a given type
///
/// This clears the signal queue for the given type and iterates over all signals
pub fn process_signals<T: Signal + 'static>(&mut self, f: impl FnMut(T)) {
self.signal.drain::<T>().for_each(f);
}
}
impl Default for UiInstance {
fn default() -> Self {
Self::new()
}
}

View file

@ -1,51 +0,0 @@
use crate::{element::UiElement, draw::UiDrawCommands};
pub struct Interactable<T: UiElement> {
pub element: T,
pub hovered: Option<Box<dyn FnOnce()>>,
pub clicked: Option<Box<dyn FnOnce()>>,
}
impl<T: UiElement> Interactable<T> {
pub fn new(element: T) -> Self {
Self {
element,
hovered: None,
clicked: None,
}
}
pub fn on_click(self, clicked: impl FnOnce() + 'static) -> Self {
Self {
clicked: Some(Box::new(clicked)),
..self
}
}
pub fn on_hover(self, clicked: impl FnOnce() + 'static) -> Self {
Self {
clicked: Some(Box::new(clicked)),
..self
}
}
}
impl<T: UiElement> UiElement for Interactable<T> {
fn measure(&self, state: &crate::state::StateRepo, layout: &crate::LayoutInfo) -> crate::measure::Response {
self.element.measure(state, layout)
}
fn process(&self, measure: &crate::measure::Response, state: &mut crate::state::StateRepo, layout: &crate::LayoutInfo, draw: &mut UiDrawCommands) {
self.element.process(measure, state, layout, draw)
}
}
pub trait IntoInteractable<T: UiElement>: UiElement {
fn into_interactable(self) -> Interactable<T>;
}
impl<T: UiElement> IntoInteractable<T> for T {
fn into_interactable(self) -> Interactable<Self> {
Interactable::new(self)
}
}

240
hui/src/layout.rs Normal file
View file

@ -0,0 +1,240 @@
//! element layout, alignment and sizing
use glam::{vec2, Vec2};
/// Controls wrapping behavior of elements
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord, Default)]
pub enum WrapBehavior {
/// No wrapping is allowed, even if explicit line breaks is requested by the element
Disable = 0,
/// Allow wrapping if the element explicitly requests it (default behavior)
#[default]
Allow = 1,
/// Elements will be wrapped automatically when they reach the maximum width/height of the container
Enable = 2,
}
impl From<bool> for WrapBehavior {
#[inline]
fn from(value: bool) -> Self {
match value {
true => Self::Enable,
false => Self::Disable,
}
}
}
impl WrapBehavior {
/// Check if wrapping is allowed for the element
#[inline]
pub fn is_allowed(&self) -> bool {
*self != Self::Disable
}
/// Check if wrapping is enabled for the element
///
/// (Wrapping will be done automatically when the element reaches the maximum width/height)
#[inline]
pub fn is_enabled(&self) -> bool {
*self == Self::Enable
}
}
/// Alignment along a single axis
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, PartialOrd, Ord)]
pub enum Alignment {
/// Put the element at the beginning of the axis\
/// (left for horizontal, top for vertical alignment)
#[default]
Begin = 0,
/// Put the element in the center
Center = 1,
/// Put the element at the end of the axis\
/// (right for horizontal, bottom for vertical alignment)
End = 2,
}
/// Represents alignment in 2D space
///
/// - `horizontal` - alignment *along* x-axis (horizontal)\
/// - `vertical` - alignment *along* y-axis (vertical)
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, PartialOrd, Ord)]
pub struct Alignment2d {
/// Alignment *along* horizontal axis (X)
///
/// ```text
/// ├───────[ ]──────┤
/// ↑↑ ↑↑ ↑↑
/// Begin Center End
/// ```
pub horizontal: Alignment,
/// Alignment *along* vertical axis (Y)
///
/// ```text
/// ┬ ←─ Begin
/// │
/// [ ] ←─ Center
/// │
/// ┴ ←─ End
/// ```
pub vertical: Alignment,
}
impl Alignment2d {
/// Create a new `Alignment2d` with the same alignment for both axes
#[inline]
pub const fn all(alignment: Alignment) -> Self {
Self {
horizontal: alignment,
vertical: alignment,
}
}
}
impl From<(Alignment, Alignment)> for Alignment2d {
#[inline]
fn from((horizontal, vertical): (Alignment, Alignment)) -> Self {
Self { horizontal, vertical }
}
}
impl From<[Alignment; 2]> for Alignment2d {
#[inline]
fn from([horizontal, vertical]: [Alignment; 2]) -> Self {
Self { horizontal, vertical }
}
}
impl From<Alignment> for Alignment2d {
#[inline]
fn from(alignment: Alignment) -> Self {
Self::all(alignment)
}
}
/// Represents a single size dimension of an UI element.\
/// Can be either a static size in pixels, a fraction the parent size or auto-calculated\
/// (Meaning of `auto` is entirely dependent on the element).
#[derive(Default, Debug, Clone, Copy, PartialEq)]
pub enum Size {
/// Automatically calculate size based on content
#[default]
Auto,
/// Static size in pixels
Absolute(f32),
/// Size as a ratio of parent element size
///
/// Expected range: `0.0..=1.0`
Relative(f32),
/// Size as a ratio of remaining space after all other elements have been laid out
///
/// Expected range: `0.0..=1.0`
///
/// - This feature is experimental and may not work as expected;\
/// Current `Container` implementation:
/// - Assumes that he line is fully filled if any element uses `Remaining` size, even if sum of remaining sizes is less than 1.0
/// - Does not support `Remaining` size in the secondary axis, it will be treated as `Relative`
/// - In cases where it's not applicable or not supported, it's defined to behave as `Relative`
Remaining(f32),
}
impl From<f32> for Size {
#[inline]
fn from(value: f32) -> Self {
Self::Absolute(value)
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq)]
pub struct Size2d {
pub width: Size,
pub height: Size,
}
impl From<(Size, Size)> for Size2d {
#[inline]
fn from((width, height): (Size, Size)) -> Self {
Self { width, height }
}
}
//XXX: should this exist?
impl From<Size> for Size2d {
#[inline]
fn from(size: Size) -> Self {
Self {
width: size,
height: size,
}
}
}
/// Represents the direction of the layout\
/// (for example, the direction of a container's children)\
///
/// - `Vertical` - Children are laid out from top to bottom\
/// - `Horizontal` - Children are laid out from left to right
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Direction {
/// Children are laid out from top to bottom
#[default]
Vertical,
/// Children are laid out from left to right
Horizontal,
}
/// Represents the layout information required to measure, layout and render an element.\
/// Includes the position, maximum size, direction of the layout and other information
pub struct LayoutInfo {
/// Screen-space coordinates of the top-left corner of the element.\
/// Use this value during the layout step to render the element
///
/// Not available during the measure step (will be set to zero)
pub position: Vec2,
/// Maximum size the element is allowed to take up
pub max_size: Vec2,
/// Current direction of the layout\
/// (Usually matches direction of the parent container)
pub direction: Direction,
/// Remaining space in the primary axis\
///
/// This value is only available during the layout step and is only likely to be present if the element uses `Size::Remaining`
///
/// (Make sure that LayoutInfo::direction is set to the correct direction!)
pub remaining_space: Option<f32>,
}
/// Helper function to calculate the size of an element based on its layout and size information\
/// Used to help reduce code duplication in the `measure` method of UI elements
pub fn compute_size(layout: &LayoutInfo, size: Size2d, comfy_size: Vec2) -> Vec2 {
let width = match size.width {
Size::Auto => comfy_size.x,
Size::Relative(fraction) => layout.max_size.x * fraction,
Size::Absolute(size) => size,
Size::Remaining(fraction) => match layout.direction {
Direction::Horizontal => layout.remaining_space.unwrap_or(layout.max_size.x) * fraction,
Direction::Vertical => layout.max_size.x * fraction,
}
};
let height = match size.height {
Size::Auto => comfy_size.y,
Size::Relative(fraction) => layout.max_size.y * fraction,
Size::Absolute(size) => size,
Size::Remaining(fraction) => match layout.direction {
Direction::Horizontal => layout.max_size.y * fraction,
Direction::Vertical => layout.remaining_space.unwrap_or(layout.max_size.y) * fraction,
}
};
vec2(width, height)
}

View file

@ -1,135 +1,29 @@
#![forbid(unsafe_code)]
#![forbid(unsafe_op_in_unsafe_fn)]
#![doc(html_logo_url = "https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/hui.svg")]
//!
//! Simple UI library for games and other interactive applications
//!
//! # Features
#![doc = document_features::document_features!()]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![deny(unsafe_code)]
#![forbid(unsafe_op_in_unsafe_fn)]
#![allow(unused_parens)]
mod instance;
mod macros;
pub mod layout;
pub mod rect;
pub mod element;
pub mod event;
pub mod input;
pub mod draw;
pub mod measure;
pub mod state;
pub mod text;
pub mod interaction;
pub mod color;
pub mod signal;
pub mod frame;
use element::UiElement;
use state::StateRepo;
use draw::{UiDrawCommands, UiDrawPlan};
use text::{TextRenderer, FontTextureInfo, FontHandle};
use glam::Vec2;
// pub struct ElementContext<'a> {
// pub state: &'a mut StateRepo,
// pub draw: &'a mut UiDrawCommands,
// pub text: &'a mut TextRenderer,
// }
pub trait IfModified<T> {
fn if_modified(&self) -> Option<&T>;
}
pub struct UiInstance {
//mouse_position: Vec2,
stateful_state: StateRepo,
//event_queue: VecDeque<UiEvent>,
prev_draw_commands: UiDrawCommands,
draw_commands: UiDrawCommands,
draw_plan: UiDrawPlan,
draw_plan_modified: bool,
text_renderer: TextRenderer,
}
impl UiInstance {
pub fn new() -> Self {
UiInstance {
//mouse_position: Vec2::ZERO,
stateful_state: StateRepo::default(),
//event_queue: VecDeque::new(),
// root_elements: Vec::new(),
prev_draw_commands: UiDrawCommands::default(),
draw_commands: UiDrawCommands::default(),
draw_plan: UiDrawPlan::default(),
draw_plan_modified: false,
// ftm: FontTextureManager::default(),
text_renderer: TextRenderer::new(),
}
}
pub fn add_font_from_bytes(&mut self, font: &[u8]) -> FontHandle {
self.text_renderer.add_font_from_bytes(font)
}
pub fn add<T: UiElement>(&mut self, element: T, max_size: Vec2) {
let layout = LayoutInfo {
position: Vec2::ZERO,
max_size,
direction: UiDirection::Vertical,
};
let measure = element.measure(&self.stateful_state, &layout);
element.process(&measure, &mut self.stateful_state, &layout, &mut self.draw_commands);
}
pub fn begin(&mut self) {
std::mem::swap(&mut self.prev_draw_commands, &mut self.draw_commands);
self.draw_plan_modified = false;
self.draw_commands.commands.clear();
self.text_renderer.reset_frame();
}
pub fn end(&mut self) {
if self.draw_commands.commands == self.prev_draw_commands.commands {
return
}
self.draw_plan = UiDrawPlan::build(&self.draw_commands, &mut self.text_renderer);
self.draw_plan_modified = true;
}
pub fn draw_plan(&self) -> (bool, &UiDrawPlan) {
(self.draw_plan_modified, &self.draw_plan)
}
pub fn font_texture(&self) -> FontTextureInfo {
self.text_renderer.font_texture()
}
}
impl Default for UiInstance {
fn default() -> Self {
Self::new()
}
}
#[derive(Default, Debug, Clone, Copy)]
pub enum UiSize {
#[default]
Auto,
Percentage(f32),
Pixels(f32),
}
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum UiDirection {
#[default]
Vertical,
Horizontal,
}
pub struct LayoutInfo {
///Not availabe during measuring step
pub position: Vec2,
pub max_size: Vec2,
pub direction: UiDirection,
}
pub struct ElementList(Vec<Box<dyn UiElement>>);
impl ElementList {
pub fn add(&mut self, element: impl UiElement + 'static) {
self.0.push(Box::new(element));
}
}
pub fn elements(f: impl FnOnce(&mut ElementList)) -> Vec<Box<dyn UiElement>> {
let mut elements = ElementList(Vec::new());
f(&mut elements);
elements.0
}
pub use instance::UiInstance;

138
hui/src/macros.rs Normal file
View file

@ -0,0 +1,138 @@
/// Constructs a `Size` or `Size2d` from a literal or expression
///
/// # Syntax:
/// - `auto` - `Size::Auto`
/// - `x` - `Size::Absolute(x)`
/// - `x%` - `Size::Relative(x / 100.)` *(literal only)*
/// - `x/` - `Size::Relative(x)`
/// - `x%=` - `Size::Remaining(x / 100.)` *(literal only)*
/// - `x/=` - `Size::Remaining(x)`
///
/// ...where `x` is a literal, identifier or an expression wrapped in parentheses
///
/// # Note:
/// - If a single argument is provided, it creates a `Size` using the rules specified above\
/// - If two arguments are provided, it creates a `Size2d` with the first value as width and the second as height\
/// Example: `size!(100, 50%)` creates a `Size2d` with width `100` (`Size::Absolute(100.)`) and height `50%` (`Size::Relative(0.5)`)
/// - `%` syntax is only valid for literals (`50%`), not expressions or identidiers.\
/// Use `/` instead (`(0.5 * x)/`, `x/`), but be aware of the different range (0.0-1.0) \
/// - Expressions must be wrapped in parentheses (for example: `(x + 5)`).\
/// This does not apply to single identifiers (`x`) or literals (`5`)
#[macro_export]
macro_rules! size {
(auto) => {
$crate::layout::Size::Auto
};
($x:literal) => {
$crate::layout::Size::Absolute($x as f32)
};
($x:literal %) => {
$crate::layout::Size::Relative($x as f32 / 100.)
};
($x:literal /) => {
$crate::layout::Size::Relative($x as f32)
};
($x:literal %=) => {
$crate::layout::Size::Remaining($x as f32 / 100.)
};
($x:literal /=) => {
$crate::layout::Size::Remaining($x as f32)
};
($x:ident) => {
$crate::layout::Size::Absolute($x as f32)
};
($x:ident /) => {
$crate::layout::Size::Relative($x as f32)
};
($x:ident /=) => {
$crate::layout::Size::Remaining($x as f32)
};
(($x:expr)) => {
$crate::layout::Size::Absolute(($x) as f32)
};
(($x:expr) /) => {
$crate::layout::Size::Relative(($x) as f32)
};
(($x:expr) /=) => {
$crate::layout::Size::Remaining(($x) as f32)
};
($x:tt , $y:tt $($ys:tt)?) => {
$crate::layout::Size2d {
width: $crate::size!($x),
height: $crate::size!($y $($ys)?),
}
};
($x:tt $($xs:tt)? , $y:tt $($ys:tt)?) => {
$crate::layout::Size2d {
width: $crate::size!($x $($xs)?),
height: $crate::size!($y $($ys)?),
}
};
}
/// Helper macro for constructing a `RectFrame`
///
/// # Example:
/// ```
/// _frame! {
/// color: (0.2, 0.2, 0.3, 1.),
/// corner_radius: 5.,
/// };
/// ```
///
/// # Note:
/// - If the `image` field is set, but not `color`, the `color` field will default to [`WHITE`](crate::color::WHITE) (to ensure visibility)
/// - If both `color` and `image` are not set, the `color` field will default to [`TRANSPARENT`](crate::color::TRANSPARENT)
#[macro_export]
macro_rules! rect_frame {
{} => {
$crate::frame::RectFrame::default()
};
// () => {
// $crate::frame::RectFrame::default()
// };
($expr:expr) => {
{
let __frame: $crate::frame::RectFrame = $crate::frame::RectFrame::from($expr);
__frame
}
};
($image:expr, $color:expr) => {
$crate::frame::RectFrame::color_image($color, $image)
};
{$($ident:ident : $expr:expr),+$(,)?} => {
{
// ensure all identifiers are unique
#[allow(non_upper_case_globals)]
{$(const $ident: () = ();)+}
// construct the RectFrame
{
let mut _frame = $crate::frame::RectFrame::default();
let mut _color_is_set = false;
let mut _image_is_set = false;
$(
{
_frame.$ident = ($expr).into();
_image_is_set |= stringify!($ident) == "image";
_color_is_set |= stringify!($ident) == "color";
}
)+
// set color to white if image is explicitly set to Some(...) but color is left as the default
if _frame.image.is_some() && _image_is_set && !_color_is_set {
_frame.color = (1., 1., 1., 1.).into();
}
_frame
}
}
};
}

View file

@ -1,7 +1,10 @@
use glam::Vec2;
//! element measurement, hints and responses
use glam::Vec2;
use crate::rect::Rect;
// #[non_exhaustive]
#[derive(Default)]
#[non_exhaustive]
pub struct Hints {
pub inner_content_size: Option<Vec2>,
pub inner_content_size_cache: Option<Vec<Vec2>>,
@ -9,7 +12,30 @@ pub struct Hints {
#[derive(Default)]
pub struct Response {
/// Computed size of the element
pub size: Vec2,
/// Hints for the layout system, can be used to optimize the layout engine.\
/// These will never cause the UI to be rendered differently (assuming the values are correct)
pub hints: Hints,
/// Arbitrary user data, can be used to pass data (for example, cache) between measure and process stages
pub user_data: Option<Box<dyn std::any::Any>>,
/// If true, the element should always cause the content to wrap to the next line\
/// (the element itself gets wrapped to the next line too)
///
/// You should almost never set this, and the exact behavior may change in the future
///
/// Currently, this forces wrapping even if Container::wrap is set to false
pub should_wrap: bool,
}
impl Response {
pub fn rect(&self, position: Vec2) -> Rect {
Rect {
position,
size: self.size,
}
}
}

15
hui/src/rect.rs Normal file
View file

@ -0,0 +1,15 @@
//! contains types which represent the sides and corners of a rectangular shape.
//XXX: this is kinda a mess, either move the rect struct here or come up with a better name for this module
#[allow(clippy::module_inception)]
mod rect;
pub use rect::Rect;
mod sides;
pub use sides::Sides;
mod corners;
pub use corners::Corners;
mod color;
pub use color::FillColor;

206
hui/src/rect/color.rs Normal file
View file

@ -0,0 +1,206 @@
use super::Corners;
use glam::{Vec2, Vec3, Vec4, vec4};
/// Represents the fill color of a rectangle
///
/// Can be a single color or a simple gradient with different colors for each corner
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FillColor(Corners<Vec4>);
impl FillColor {
pub const fn new(corners: Corners<Vec4>) -> Self {
Self(corners)
}
/// Transparent background (alpha = 0)
pub const TRANSPARENT: Self = Self::rgba(0., 0., 0., 0.);
/// Transparent background (alpha = 0)
pub const fn transparent() -> Self {
Self::TRANSPARENT
}
/// Check if the fill color is completely transparent
///
/// (i.e. all corners have an alpha value of 0.0)
pub fn is_transparent(&self) -> bool {
self.0.top_left.w == 0. &&
self.0.top_right.w == 0. &&
self.0.bottom_left.w == 0. &&
self.0.bottom_right.w == 0.
}
/// Check if the fill color is completely opaque
///
/// (i.e. all corners have an alpha value of 1.0)
pub fn is_opaque(&self) -> bool {
self.0.top_left.w == 1. &&
self.0.top_right.w == 1. &&
self.0.bottom_left.w == 1. &&
self.0.bottom_right.w == 1.
}
/// Construct a solid color fill from values representing the red, green, blue and alpha channels
pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
Self(Corners {
top_left: vec4(r, g, b, a),
top_right: vec4(r, g, b, a),
bottom_left: vec4(r, g, b, a),
bottom_right: vec4(r, g, b, a),
})
}
/// Construct a solid color fill from three values representing the red, green and blue channels
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
Self(Corners {
top_left: vec4(r, g, b, 1.0),
top_right: vec4(r, g, b, 1.0),
bottom_left: vec4(r, g, b, 1.0),
bottom_right: vec4(r, g, b, 1.0),
})
}
/// Construct a solid color fill from colors for each corner
pub const fn from_corners(corners: Corners<Vec4>) -> Self {
Self(corners)
}
/// Get a list of the colors for each corner
pub const fn corners(&self) -> Corners<Vec4> {
self.0
}
/// Interpolate color on position, assuming a linear gradient
pub fn interpolate(&self, uv: Vec2) -> Vec4 {
let c = self.corners();
let top = c.top_left.lerp(c.top_right, uv.x);
let bottom = c.bottom_left.lerp(c.bottom_right, uv.x);
top.lerp(bottom, uv.y)
}
}
impl Default for FillColor {
fn default() -> Self {
Self(Corners::all(vec4(0.0, 0.0, 0.0, 1.0)))
}
}
impl From<Corners<Vec4>> for FillColor {
fn from(corners: Corners<Vec4>) -> Self {
Self(corners)
}
}
impl From<FillColor> for Corners<Vec4> {
fn from(corners: FillColor) -> Self {
corners.0
}
}
impl From<Vec4> for FillColor {
fn from(value: Vec4) -> Self {
Self(Corners::all(value))
}
}
impl From<(f32, f32, f32, f32)> for FillColor {
fn from((r, g, b, a): (f32, f32, f32, f32)) -> Self {
Self(Corners::all(vec4(r, g, b, a)))
}
}
impl From<[f32; 4]> for FillColor {
fn from([r, g, b, a]: [f32; 4]) -> Self {
Self(Corners::all(vec4(r, g, b, a)))
}
}
impl From<Vec3> for FillColor {
fn from(value: Vec3) -> Self {
Self(Corners::all(vec4(value.x, value.y, value.z, 1.0)))
}
}
impl From<(f32, f32, f32)> for FillColor {
fn from((r, g, b): (f32, f32, f32)) -> Self {
Self(Corners::all(vec4(r, g, b, 1.0)))
}
}
impl From<[f32; 3]> for FillColor {
fn from([r, g, b]: [f32; 3]) -> Self {
Self(Corners::all(vec4(r, g, b, 1.0)))
}
}
impl From<(Vec4, Vec4, Vec4, Vec4)> for FillColor {
fn from((top_left, top_right, bottom_left, bottom_right): (Vec4, Vec4, Vec4, Vec4)) -> Self {
Self(Corners { top_left, top_right, bottom_left, bottom_right })
}
}
impl From<((f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32))> for FillColor {
fn from(value: ((f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32))) -> Self {
Self(Corners {
top_left: vec4(value.0.0, value.0.1, value.0.2, value.0.3),
top_right: vec4(value.1.0, value.1.1, value.1.2, value.1.3),
bottom_left: vec4(value.2.0, value.2.1, value.2.2, value.2.3),
bottom_right: vec4(value.3.0, value.3.1, value.3.2, value.3.3),
})
}
}
impl From<[[f32; 4]; 4]> for FillColor {
fn from(value: [[f32; 4]; 4]) -> Self {
Self(Corners {
top_left: vec4(value[0][0], value[0][1], value[0][2], value[0][3]),
top_right: vec4(value[1][0], value[1][1], value[1][2], value[1][3]),
bottom_left: vec4(value[2][0], value[2][1], value[2][2], value[2][3]),
bottom_right: vec4(value[3][0], value[3][1], value[3][2], value[3][3]),
})
}
}
impl From<Corners<Vec3>> for FillColor {
fn from(corners: Corners<Vec3>) -> Self {
Self(Corners {
top_left: corners.top_left.extend(1.),
top_right: corners.top_right.extend(1.),
bottom_left: corners.bottom_left.extend(1.),
bottom_right: corners.bottom_right.extend(1.),
})
}
}
impl From<(Vec3, Vec3, Vec3, Vec3)> for FillColor {
fn from((top_left, top_right, bottom_left, bottom_right): (Vec3, Vec3, Vec3, Vec3)) -> Self {
Self(Corners {
top_left: vec4(top_left.x, top_left.y, top_left.z, 1.0),
top_right: vec4(top_right.x, top_right.y, top_right.z, 1.0),
bottom_left: vec4(bottom_left.x, bottom_left.y, bottom_left.z, 1.0),
bottom_right: vec4(bottom_right.x, bottom_right.y, bottom_right.z, 1.0),
})
}
}
impl From<((f32, f32, f32), (f32, f32, f32), (f32, f32, f32), (f32, f32, f32))> for FillColor {
fn from(value: ((f32, f32, f32), (f32, f32, f32), (f32, f32, f32), (f32, f32, f32))) -> Self {
Self(Corners {
top_left: vec4(value.0.0, value.0.1, value.0.2, 1.0),
top_right: vec4(value.1.0, value.1.1, value.1.2, 1.0),
bottom_left: vec4(value.2.0, value.2.1, value.2.2, 1.0),
bottom_right: vec4(value.3.0, value.3.1, value.3.2, 1.0),
})
}
}
impl From<[[f32; 3]; 4]> for FillColor {
fn from(value: [[f32; 3]; 4]) -> Self {
Self(Corners {
top_left: vec4(value[0][0], value[0][1], value[0][2], 1.0),
top_right: vec4(value[1][0], value[1][1], value[1][2], 1.0),
bottom_left: vec4(value[2][0], value[2][1], value[2][2], 1.0),
bottom_right: vec4(value[3][0], value[3][1], value[3][2], 1.0),
})
}
}

85
hui/src/rect/corners.rs Normal file
View file

@ -0,0 +1,85 @@
/// Represents 4 corners of a rectangular shape.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct Corners<T> {
pub top_left: T,
pub top_right: T,
pub bottom_left: T,
pub bottom_right: T,
}
impl<T: Clone> Corners<T> {
#[inline]
pub fn all(value: T) -> Self {
Self {
top_left: value.clone(),
top_right: value.clone(),
bottom_left: value.clone(),
bottom_right: value,
}
}
#[inline]
pub fn top_bottom(top: T, bottom: T) -> Self {
Self {
top_left: top.clone(),
top_right: top,
bottom_left: bottom.clone(),
bottom_right: bottom,
}
}
#[inline]
pub fn left_right(left: T, right: T) -> Self {
Self {
top_left: left.clone(),
top_right: right.clone(),
bottom_left: left,
bottom_right: right,
}
}
}
impl <T: Ord + Clone> Corners<T> {
pub fn max(&self) -> T {
self.top_left.clone()
.max(self.top_right.clone())
.max(self.bottom_left.clone())
.max(self.bottom_right.clone())
.clone()
}
}
impl Corners<f32> {
pub fn max_f32(&self) -> f32 {
self.top_left
.max(self.top_right)
.max(self.bottom_left)
.max(self.bottom_right)
}
}
impl Corners<f64> {
pub fn max_f64(&self) -> f64 {
self.top_left
.max(self.top_right)
.max(self.bottom_left)
.max(self.bottom_right)
}
}
impl<T: Clone> From<T> for Corners<T> {
fn from(value: T) -> Self {
Self::all(value)
}
}
impl<T> From<(T, T, T, T)> for Corners<T> {
fn from((top_left, top_right, bottom_left, bottom_right): (T, T, T, T)) -> Self {
Self {
top_left,
top_right,
bottom_left,
bottom_right,
}
}
}

138
hui/src/rect/rect.rs Normal file
View file

@ -0,0 +1,138 @@
use glam::Vec2;
use super::Corners;
/// Represents a rectangle/AABB with specified position and size
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct Rect {
/// Position of the top-left corner of the rect.
pub position: Vec2,
/// Size of the rect, should not be negative.
pub size: Vec2,
}
impl Rect {
pub const fn new(position: Vec2, size: Vec2) -> Self {
Self { position, size }
}
pub const fn from_position(position: Vec2) -> Self {
Self {
position,
size: Vec2::ZERO,
}
}
pub const fn from_size(size: Vec2) -> Self {
Self {
position: Vec2::ZERO,
size,
}
}
/// Check if the rect contains a point.
pub fn contains_point(&self, point: Vec2) -> bool {
point.cmpge(self.position).all() && point.cmple(self.position + self.size).all()
}
//TODO: return intersect rect
/// Check if the rect intersects with another rect.
pub fn intersects_rect(&self, other: Rect) -> bool {
self.position.x < other.position.x + other.size.x
&& self.position.x + self.size.x > other.position.x
&& self.position.y < other.position.y + other.size.y
&& self.position.y + self.size.y > other.position.y
}
/// Get width of the rectangle.
///
/// To get both width and height, use the `size` property instead.
pub fn width(&self) -> f32 {
self.size.x
}
/// Get height of the rectangle.
///
/// To get both width and height, use the `size` property instead.
pub fn height(&self) -> f32 {
self.size.y
}
/// Get position of the top-left corner of the rectangle on the x-axis.
///
/// To get both x and y, use the `position` property instead.
pub fn x(&self) -> f32 {
self.position.x
}
/// Get position of the top-left corner of the rectangle on the y-axis.
///
/// To get both x and y, use the `position` property instead.
pub fn y(&self) -> f32 {
self.position.y
}
/// Get positions of all 4 corners of the rectangle.
pub fn corners(&self) -> Corners<Vec2> {
Corners {
top_left: self.position,
top_right: self.position + Vec2::new(self.size.x, 0.0),
bottom_left: self.position + Vec2::new(0.0, self.size.y),
bottom_right: self.position + self.size,
}
}
}
impl From<Vec2> for Rect {
/// Create a new `Rect` from a `Vec2`, where x and y are the width and height of the rect respectively.
fn from(size: Vec2) -> Self {
Self::from_size(size)
}
}
impl From<(Vec2, Vec2)> for Rect {
/// Create a new `Rect` from a tuple of two `Vec2`s, where the first `Vec2` is the position and the second `Vec2` is the size.
fn from((position, size): (Vec2, Vec2)) -> Self {
Self { position, size }
}
}
impl From<(f32, f32, f32, f32)> for Rect {
/// Create a new `Rect` from a tuple of 4 `f32`s, where the first two `f32`s are the x and y positions of the top-left corner and the last two `f32`s are the width and height of the rect respectively.
fn from((x, y, width, height): (f32, f32, f32, f32)) -> Self {
Self {
position: Vec2::new(x, y),
size: Vec2::new(width, height),
}
}
}
impl From<[f32; 4]> for Rect {
/// Create a new `Rect` from an array of 4 `f32`s, where the first two `f32`s are the x and y positions of the top-left corner and the last two `f32`s are the width and height of the rect respectively.
fn from([x, y, width, height]: [f32; 4]) -> Self {
Self {
position: Vec2::new(x, y),
size: Vec2::new(width, height),
}
}
}
impl From<Rect> for (Vec2, Vec2) {
/// Convert a `Rect` into a tuple of two `Vec2`s, where the first `Vec2` is the position and the second `Vec2` is the size.
fn from(rect: Rect) -> Self {
(rect.position, rect.size)
}
}
impl From<Rect> for (f32, f32, f32, f32) {
/// Convert a `Rect` into a tuple of 4 `f32`s, where the first two `f32`s are the x and y positions of the top-left corner and the last two `f32`s are the width and height of the rect respectively.
fn from(rect: Rect) -> Self {
(rect.position.x, rect.position.y, rect.size.x, rect.size.y)
}
}
impl From<Rect> for [f32; 4] {
/// Convert a `Rect` into an array of 4 `f32`s, where the first two `f32`s are the x and y positions of the top-left corner and the last two `f32`s are the width and height of the rect respectively.
fn from(rect: Rect) -> Self {
[rect.position.x, rect.position.y, rect.size.x, rect.size.y]
}
}

49
hui/src/rect/sides.rs Normal file
View file

@ -0,0 +1,49 @@
/// Represents 4 sides of a rectangular shape.
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
pub struct Sides<T> {
pub top: T,
pub bottom: T,
pub left: T,
pub right: T,
}
impl<T: Clone> Sides<T> {
#[inline]
pub fn all(value: T) -> Self {
Self {
top: value.clone(),
bottom: value.clone(),
left: value.clone(),
right: value,
}
}
#[inline]
pub fn horizontal_vertical(horizontal: T, vertical: T) -> Self {
Self {
top: vertical.clone(),
bottom: vertical,
left: horizontal.clone(),
right: horizontal,
}
}
}
impl<T: Clone> From<T> for Sides<T> {
fn from(value: T) -> Self {
Self::all(value)
}
}
impl<T: Clone> From<(T, T)> for Sides<T> {
fn from((horizontal, vertical): (T, T)) -> Self {
Self::horizontal_vertical(horizontal, vertical)
}
}
impl<T> From<(T, T, T, T)> for Sides<T> {
fn from((top, bottom, left, right): (T, T, T, T)) -> Self {
Self { top, bottom, left, right }
}
}

105
hui/src/signal.rs Normal file
View file

@ -0,0 +1,105 @@
//! signal handling for UI events
use std::any::{Any, TypeId};
use hashbrown::HashMap;
use nohash_hasher::BuildNoHashHasher;
pub mod trigger;
#[cfg(feature = "derive")]
pub use hui_derive::Signal;
/// A marker trait for UI Signals
pub trait Signal: Any {}
// #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
// pub(crate) struct DummySignal;
// impl UiSignal for DummySignal {}
/// Internal storage for signals
pub struct SignalStore {
//TODO use a multithreaded queue instead, to allow easily offloading ui processing to a different thread
///XXX: is this truly the most efficient structure?
sig: HashMap<TypeId, Vec<Box<dyn Any>>, BuildNoHashHasher<u64>>
}
impl SignalStore {
/// Create a new [`SigIntStore`]
pub(crate) fn new() -> Self {
Self {
sig: Default::default(),
}
}
/// Ensure that store for given signal type exists and return a mutable reference to it
fn internal_store<T: Signal + 'static>(&mut self) -> &mut Vec<Box<dyn Any>> {
let type_id = TypeId::of::<T>();
self.sig.entry(type_id).or_default()
}
/// Add a signal to the store
///
/// Signals are stored in the order they are added
pub fn add<T: Signal + 'static>(&mut self, sig: T) {
let type_id = TypeId::of::<T>();
if let Some(v) = self.sig.get_mut(&type_id) {
v.push(Box::new(sig));
} else {
self.sig.insert(type_id, vec![Box::new(sig)]);
}
}
/// Drain all signals of a given type
pub(crate) fn drain<T: Signal + 'static>(&mut self) -> impl Iterator<Item = T> + '_ {
self.internal_store::<T>()
.drain(..)
.map(|x| *x.downcast::<T>().unwrap()) //unchecked?
}
/// Clear all signals
pub(crate) fn clear(&mut self) {
//XXX: should we clear the vecs instead?
self.sig.clear();
}
}
// pub trait Signal {
// type Arg;
// type Output;
// fn call(&self, arg: Self::Arg) -> Self::Output;
// }
// impl<F: Fn() -> T, T> Signal for F {
// type Arg = ();
// type Output = T;
// fn call(&self, _: Self::Arg) -> Self::Output {
// self()
// }
// }
// // impl<F: Fn(A) -> T, A, T> Signal for F {
// // type Arg = A;
// // type Output = T;
// // fn call(&self, a: Self::Arg) -> Self::Output {
// // self(a)
// // }
// // }
// pub struct SignalTrigger<R: UiSignal + 'static, A = ()>(pub(crate) Box<dyn Fn(A) -> R + 'static>);
// impl<R: UiSignal + 'static, A> SignalTrigger<R, A> {
// pub fn new<F: Fn(A) -> R + 'static>(f: F) -> Self {
// Self(Box::new(f))
// }
// pub fn call(&self, a: A) -> R {
// (self.0)(a)
// }
// }
// impl<R: UiSignal + 'static, A, T: Fn(A) -> R + 'static> From<T> for SignalTrigger<R, A> {
// fn from(f: T) -> Self {
// Self(Box::new(f))
// }
// }

71
hui/src/signal/trigger.rs Normal file
View file

@ -0,0 +1,71 @@
//! Contains the implementation of signal triggers, which simplify creation of custom elements
use super::{Signal, SignalStore};
//use crate::element::UiElement;
/// Signal trigger that does not take any arguments
#[allow(clippy::complexity)]
pub struct SignalTrigger(Box<dyn Fn(&mut SignalStore)>);
impl SignalTrigger {
/// Create a new signal trigger from a function or a constructor
pub fn new<S: Signal + 'static, F: Fn() -> S + 'static>(f: F) -> Self {
Self(Box::new(move |s: &mut SignalStore| {
s.add::<S>(f());
}))
}
/// Fire the signal
pub fn fire(&self, s: &mut SignalStore) {
(self.0)(s);
}
}
/// Signal trigger that takes a single argument and passes it to the signal
#[allow(clippy::complexity)]
pub struct SignalTriggerArg<T>(Box<dyn Fn(&mut SignalStore, T)>);
impl<T> SignalTriggerArg<T> {
/// Create a new signal trigger from a function or a constructor
pub fn new<S: Signal, F: Fn(T) -> S + 'static>(f: F) -> Self {
Self(Box::new(move |s: &mut SignalStore, x| {
s.add::<S>(f(x));
}))
}
/// Fire the signal with the given argument
pub fn fire(&self, s: &mut SignalStore, x: T) {
(self.0)(s, x);
}
}
// #[allow(clippy::complexity)]
// pub struct SignalTriggerElement<E: UiElement>(Box<dyn Fn(&mut SignalStore, &mut E)>);
// impl<E: UiElement> SignalTriggerElement<E> {
// pub fn new<S: Signal, F: Fn(&mut E) -> S + 'static>(f: F) -> Self {
// Self(Box::new(move |s: &mut SignalStore, e: &mut E| {
// s.add::<S>(f(e));
// }))
// }
// pub fn fire(&self, s: &mut SignalStore, e: &mut E) {
// (self.0)(s, e);
// }
// }
// #[allow(clippy::complexity)]
// pub struct SignalTriggerElementArg<E: UiElement, T>(Box<dyn Fn(&mut SignalStore, &mut E, T)>);
// impl<E: UiElement, T> SignalTriggerElementArg<E, T> {
// pub fn new<S: Signal, F: Fn(T, &mut E) -> S + 'static>(f: F) -> Self {
// Self(Box::new(move |s: &mut SignalStore, e: &mut E, x| {
// s.add::<S>(f(x, e));
// }))
// }
// pub fn fire(&self, s: &mut SignalStore, e: &mut E, x: T) {
// (self.0)(s, e, x);
// }
// }

View file

@ -1,9 +1,142 @@
//! state managment for stateful elements
use hashbrown::{HashMap, HashSet};
use nohash_hasher::BuildNoHashHasher;
use std::any::Any;
use std::{any::Any, hash::{Hash, Hasher}};
use rustc_hash::FxHasher;
//TODO impl StateRepo functions and automatic cleanup of inactive ids
#[cfg(feature = "derive")]
pub use hui_derive::State;
/// Marker trait for state objects
pub trait State: Any {}
/// Integer type used to identify a state object
type StateId = u64;
fn hash_local(x: impl Hash, g: &[StateId]) -> StateId {
let mut hasher = FxHasher::default();
0xdeadbeefu64.hash(&mut hasher);
for x in g {
x.hash(&mut hasher);
}
x.hash(&mut hasher);
hasher.finish()
}
fn hash_global(x: impl Hash) -> StateId {
let mut hasher = FxHasher::default();
0xcafebabeu64.hash(&mut hasher);
x.hash(&mut hasher);
hasher.finish()
}
#[derive(Default)]
pub struct StateRepo {
state: HashMap<u64, Box<dyn Any>, BuildNoHashHasher<u64>>,
active_ids: HashSet<u64, BuildNoHashHasher<u64>>
/// Stack of ids used to identify state objects
id_stack: Vec<StateId>,
/// Implementation detail: used to prevent needlessly reallocating the id stack if the `global`` function is used
standby: Vec<StateId>,
/// Actual state objects
state: HashMap<StateId, Box<dyn Any>, BuildNoHashHasher<StateId>>,
/// IDs that were accessed during the current frame, everything else is considered inactive and can be cleaned up
active_ids: HashSet<StateId, BuildNoHashHasher<StateId>>
}
impl StateRepo {
/// Push an id to the stack
pub fn push(&mut self, id: impl Hash) {
self.id_stack.push(hash_global(id));
}
/// Pop the last id from the stack
///
/// ## Panics:
/// Panics if the stack is empty
pub fn pop(&mut self) {
self.id_stack.pop().expect("stack is empty");
}
/// Create a new [`StateRepo`]
pub(crate) fn new() -> Self {
Self::default()
}
/// Get a reference to a state object by its id
pub fn acquire<T: State>(&mut self, id: impl Hash) -> Option<&T> {
let id = hash_local(id, &self.id_stack);
self.active_ids.insert(id);
self.state.get(&id).unwrap().downcast_ref::<T>()
}
/// Get a reference to a state object by its id or insert a new one
pub fn acquire_or_insert<T: State>(&mut self, id: impl Hash, state: T) -> &T {
let id = hash_local(id, &self.id_stack);
self.state.entry(id)
.or_insert_with(|| Box::new(state))
.downcast_ref::<T>().unwrap()
}
/// Get a reference to a state object by its id or insert a new default one
pub fn acquire_or_default<T: State + Default>(&mut self, id: impl Hash) -> &T {
let id = hash_local(id, &self.id_stack);
self.state.entry(id)
.or_insert_with(|| Box::<T>::default())
.downcast_ref::<T>().unwrap()
}
/// Get a mutable reference to a state object by its id
pub fn acquire_mut<T: State>(&mut self, id: impl Hash) -> Option<&mut T> {
let id = hash_local(id, &self.id_stack);
self.active_ids.insert(id);
self.state.get_mut(&id).unwrap().downcast_mut::<T>()
}
/// Get a mutable reference to a state object by its id or insert a new one
pub fn acquire_mut_or_insert<T: State>(&mut self, id: impl Hash, state: T) -> &mut T {
let id = hash_local(id, &self.id_stack);
self.state.entry(id)
.or_insert_with(|| Box::new(state))
.downcast_mut::<T>().unwrap()
}
/// Get a mutable reference to a state object by its id or insert a new default one
pub fn acquire_mut_or_default<T: State + Default>(&mut self, id: impl Hash) -> &mut T {
let id = hash_local(id, &self.id_stack);
self.state.entry(id)
.or_insert_with(|| Box::<T>::default())
.downcast_mut::<T>().unwrap()
}
/// Temporarily forget about current id stack, and use an empty one (within the context of the closure)
///
/// Can be useful for state management of non-hierarchical objects, e.g. popups
pub fn global<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
self.standby.clear();
std::mem::swap(&mut self.id_stack, &mut self.standby);
let ret = f(self);
std::mem::swap(&mut self.id_stack, &mut self.standby);
ret
}
/// Scope the state repo
///
/// Anything pushed or popped will be discarded after the closure,
/// and the stack will be restored to its previous state
pub fn scope<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
self.standby.clear();
self.standby.extend(self.id_stack.iter().copied());
let ret = f(self);
std::mem::swap(&mut self.id_stack, &mut self.standby);
ret
//XXX: this is super efficient, but works only for pushes, if anything is popped, it will be lost
// let len = self.id_stack.len();
// let ret = f(self);
// self.id_stack.truncate(len);
}
}

View file

@ -1,45 +1,60 @@
//! text rendering, styling, measuring
use std::sync::Arc;
use fontdue::{Font, FontSettings};
use crate::draw::atlas::TextureAtlasManager;
mod font;
mod ftm;
mod stack;
/// Built-in font handle
#[cfg(feature="builtin_font")]
pub use font::BUILTIN_FONT;
pub use font::FontHandle;
use font::FontManager;
pub use font::FontHandle;
use fontdue::{Font, FontSettings};
use ftm::FontTextureManager;
pub use ftm::{FontTextureInfo, GlyphCacheEntry};
use ftm::GlyphCacheEntry;
use stack::FontStack;
pub struct TextRenderer {
fm: FontManager,
pub(crate) struct TextRenderer {
manager: FontManager,
ftm: FontTextureManager,
stack: FontStack,
}
impl TextRenderer {
pub fn new() -> Self {
Self {
fm: FontManager::new(),
manager: FontManager::new(),
ftm: FontTextureManager::default(),
stack: FontStack::new(),
}
}
pub fn add_font_from_bytes(&mut self, font: &[u8]) -> FontHandle {
self.fm.add_font(Font::from_bytes(font, FontSettings::default()).unwrap())
self.manager.add_font(Font::from_bytes(font, FontSettings::default()).unwrap())
}
pub fn reset_frame(&mut self) {
self.ftm.reset_modified();
pub fn glyph(&mut self, atlas: &mut TextureAtlasManager, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> {
self.ftm.glyph(atlas, &self.manager, font_handle, character, size)
}
pub fn font_texture(&self) -> FontTextureInfo {
self.ftm.info()
pub fn push_font(&mut self, font: FontHandle) {
self.stack.push(font);
}
pub fn glyph(&mut self, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> {
self.ftm.glyph(&self.fm, font_handle, character, size)
pub fn pop_font(&mut self) {
self.stack.pop();
}
pub fn current_font(&self) -> FontHandle {
self.stack.current_or_default()
}
pub(crate) fn internal_font(&self, handle: FontHandle) -> &Font {
self.fm.get(handle).unwrap()
self.manager.get(handle).unwrap()
}
}
@ -48,3 +63,44 @@ impl Default for TextRenderer {
Self::new()
}
}
/// Size of measured text
pub struct TextMeasureResponse {
pub max_width: f32,
pub height: f32,
}
/// Context for measuring text
#[derive(Clone, Copy)]
pub struct TextMeasure<'a>(&'a TextRenderer);
impl<'a> TextMeasure<'a> {
/// Measure the given string of text with the given font and size
pub fn measure(&self, font: FontHandle, size: u16, text: &str) -> TextMeasureResponse {
use fontdue::layout::{Layout, CoordinateSystem, TextStyle};
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.append(
&[self.0.internal_font(font)],
&TextStyle::new(text, size as f32, 0)
);
TextMeasureResponse {
max_width: layout.lines().map(|lines| {
lines.iter().fold(0.0_f32, |acc, x| {
let glyph = layout.glyphs().get(x.glyph_end).unwrap();
acc.max(glyph.x + glyph.width as f32)
})
}).unwrap_or(0.),
height: layout.height(),
}
}
}
impl TextRenderer {
pub fn to_measure(&self) -> TextMeasure {
TextMeasure(self)
}
pub fn measure(&self, font: FontHandle, size: u16, text: &str) -> TextMeasureResponse {
TextMeasure(self).measure(font, size, text)
}
}

View file

@ -1,14 +1,32 @@
use fontdue::Font;
#[cfg(feature = "builtin_font")]
const BIN_FONT: &[u8] = include_bytes!("../../assets/font/ProggyTiny.ttf");
/// Font handle, stores the internal font id and can be cheaply copied.
///
/// Only valid for the `UiInstance` that created it.\
/// Using it with other instances may result in panics or unexpected behavior.
///
/// Handle values are not guaranteed to be valid.\
/// Creating or transmuting an invalid handle is allowed and is *not* UB.
///
/// Internal value is an implementation detail and should not be relied upon.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct FontHandle(pub(crate) usize);
#[cfg(feature = "builtin_font")]
pub const BUILTIN_FONT: FontHandle = FontHandle(0);
impl Default for FontHandle {
/// Default font handle is the builtin font, if the feature is enabled;\
/// Otherwise returns an invalid handle.
fn default() -> Self {
#[cfg(feature = "builtin_font")] { BUILTIN_FONT }
#[cfg(not(feature = "builtin_font"))] { Self(usize::MAX) }
}
}
#[cfg(feature = "builtin_font")]
const BUILTIN_FONT_DATA: &[u8] = include_bytes!("../../assets/font/ProggyTiny.ttf");
pub struct FontManager {
fonts: Vec<Font>,
}
@ -20,7 +38,10 @@ impl FontManager {
};
#[cfg(feature = "builtin_font")]
{
let font = Font::from_bytes(BIN_FONT, fontdue::FontSettings::default()).unwrap();
let font = Font::from_bytes(
BUILTIN_FONT_DATA,
fontdue::FontSettings::default()
).unwrap();
this.add_font(font);
};
this

View file

@ -1,10 +1,7 @@
use std::sync::Arc;
use fontdue::Metrics;
use glam::{IVec2, UVec2, uvec2, ivec2};
use hashbrown::HashMap;
use rect_packer::DensePacker;
use crate::IfModified;
use crate::draw::atlas::{TextureAtlasManager, ImageHandle};
use super::font::{FontHandle, FontManager};
@ -16,70 +13,30 @@ struct GlyphCacheKey {
}
pub struct GlyphCacheEntry {
pub data: Vec<u8>,
pub metrics: Metrics,
pub position: IVec2,
pub size: UVec2,
pub texture: ImageHandle,
}
#[derive(Clone, Copy, Debug)]
pub struct FontTextureInfo<'a> {
pub modified: bool,
pub data: &'a [u8],
pub size: UVec2,
}
impl<'a> IfModified<FontTextureInfo<'a>> for FontTextureInfo<'a> {
fn if_modified(&self) -> Option<&Self> {
match self.modified {
true => Some(self),
false => None,
}
}
}
// impl<'a> FontTextureInfo<'a> {
// fn if_modified(&self) -> Option<Self> {
// match self.modified {
// true => Some(*self),
// false => None,
// }
// }
// }
pub struct FontTextureManager {
glyph_cache: HashMap<GlyphCacheKey, Arc<GlyphCacheEntry>>,
packer: DensePacker,
font_texture: Vec<u8>,
font_texture_size: UVec2,
modified: bool,
glyph_cache: HashMap<GlyphCacheKey, Arc<GlyphCacheEntry>>
}
impl FontTextureManager {
pub fn new(size: UVec2) -> Self {
pub fn new() -> Self {
FontTextureManager {
glyph_cache: HashMap::new(),
packer: DensePacker::new(size.x as i32, size.y as i32),
font_texture: vec![0; (size.x * size.y * 4) as usize],
font_texture_size: size,
modified: false,
}
}
pub fn reset_modified(&mut self) {
self.modified = false;
}
pub fn info(&self) -> FontTextureInfo {
FontTextureInfo {
modified: self.modified,
data: &self.font_texture,
size: self.font_texture_size,
}
}
/// Either looks up the glyph in the cache or renders it and adds it to the cache.
pub fn glyph(&mut self, font_manager: &FontManager, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> {
pub fn glyph(
&mut self,
atlas: &mut TextureAtlasManager,
font_manager: &FontManager,
font_handle: FontHandle,
character: char,
size: u8
) -> Arc<GlyphCacheEntry> {
let key = GlyphCacheKey {
font_index: font_handle.0,
character,
@ -90,38 +47,16 @@ impl FontTextureManager {
}
let font = font_manager.get(font_handle).unwrap();
let (metrics, bitmap) = font.rasterize(character, size as f32);
log::debug!("rasterized glyph: {}, {:?}, {:?}", character, metrics, bitmap);
let texture_position = self.packer.pack(metrics.width as i32, metrics.height as i32, false).unwrap();
let texture_size = uvec2(metrics.width as u32, metrics.height as u32);
log::trace!("rasterized glyph: {}, {:?}, {:?}", character, metrics, bitmap);
let texture = atlas.add_grayscale(metrics.width, &bitmap);
let entry = Arc::new(GlyphCacheEntry {
data: bitmap,
metrics,
position: ivec2(texture_position.x, texture_position.y),
size: texture_size,
texture
});
self.glyph_cache.insert_unique_unchecked(key, Arc::clone(&entry));
self.glyph_place(&entry);
self.modified = true;
entry
}
/// Place glyph onto the font texture.
fn glyph_place(&mut self, entry: &GlyphCacheEntry) {
let tex_size = self.font_texture_size;
let GlyphCacheEntry { size, position, data, .. } = entry;
//println!("{size:?} {position:?}");
for y in 0..size.y {
for x in 0..size.x {
let src = (size.x * y + x) as usize;
let dst = (tex_size.x * (y + position.y as u32) + (x + position.x as u32)) as usize * 4;
self.font_texture[dst..=(dst + 3)].copy_from_slice(&[255, 255, 255, data[src]]);
//print!("{} ", if data[src] > 0 {'#'} else {'.'});
//print!("{src} {dst} / ");
}
//println!();
}
}
// pub fn glyph(&mut self, font_manager: &FontManager, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> {
// let (is_new, glyph) = self.glyph_allocate(font_manager, font_handle, character, size);
// if is_new {
@ -133,7 +68,5 @@ impl FontTextureManager {
}
impl Default for FontTextureManager {
fn default() -> Self {
Self::new(uvec2(1024, 1024))
}
fn default() -> Self { Self::new() }
}

32
hui/src/text/stack.rs Normal file
View file

@ -0,0 +1,32 @@
use super::FontHandle;
pub struct FontStack {
fonts: Vec<FontHandle>,
}
impl FontStack {
pub fn new() -> Self {
Self {
#[cfg(not(feature = "builtin_font"))]
fonts: Vec::new(),
#[cfg(feature = "builtin_font")]
fonts: vec![super::BUILTIN_FONT],
}
}
pub fn push(&mut self, font: FontHandle) {
self.fonts.push(font);
}
pub fn pop(&mut self) {
assert!(self.fonts.pop().is_some())
}
pub fn current(&self) -> Option<FontHandle> {
self.fonts.last().copied()
}
pub fn current_or_default(&self) -> FontHandle {
self.current().unwrap_or_default()
}
}