Compare commits

...

278 commits

Author SHA1 Message Date
griffi-gh 6eb3d98ad4 k it kinda wokrs 2024-05-08 03:21:49 +02:00
griffi-gh b6dc8f370b initial wgpu backend impl 2024-05-08 02:57:03 +02:00
griffi-gh b188e49a3c wip wgpu be 2024-05-08 01:50:14 +02:00
griffi-gh b71be4f9fd upd readme to mention wgpu 2024-05-08 01:50:02 +02:00
griffi-gh 82862a554f remove unused thingy 2024-05-08 01:49:48 +02:00
griffi-gh 20be787a62 create hui-wgpu 2024-05-05 01:14:06 +02:00
griffi-gh 3a50d2d0dd add support for winit 0.30 2024-05-04 21:43:03 +02:00
griffi-gh e7b2a8ff69 skip rendering transparent rects 2024-05-04 17:23:39 +02:00
griffi-gh 2863bbdf8a rename stack_bottom to stack_below 2024-04-22 01:14:43 +02:00
griffi-gh f8ee8d0e70 for rgb stuff, always return true instead of delegating 2024-04-17 16:28:57 +02:00
griffi-gh b30fe304d1 use rect om frame api 2024-04-17 16:22:51 +02:00
griffi-gh 104ac018fe uwu 2024-04-17 16:04:34 +02:00
griffi-gh 7a4c4b1a29 a 2024-04-17 16:02:29 +02:00
griffi-gh eda2ddcc0f refactor rect_frame macro 2024-04-17 16:00:27 +02:00
griffi-gh d82eb2eeb1 rename FrameRect to RectFrame 2024-04-17 15:57:46 +02:00
griffi-gh 44b8beaa94 lol i forgor to rename the module itself 2024-04-17 15:47:22 +02:00
griffi-gh 45829d5fef rename FillRect to FrameView 2024-04-17 15:44:47 +02:00
griffi-gh 7b671d2f3d uhh stuff 2024-04-17 15:41:10 +02:00
griffi-gh cdaf4c0781 allow rect in add, clean up elem api 2024-04-17 15:21:17 +02:00
griffi-gh 92f8975702 add state scope function 2024-03-29 14:27:57 +01:00
griffi-gh aa9e0de3e9 wip state stuff 2024-03-29 13:44:56 +01:00
griffi-gh ec4404b26c
Update devcontainer.json 2024-03-26 00:40:54 +01:00
griffi-gh 9e9cf7d1c2
Update devcontainer.json 2024-03-26 00:36:04 +01:00
griffi-gh bf7244ea33
Create devcontainer.json 2024-03-26 00:25:28 +01:00
griffi-gh 787b20b3db bump master version to 0.1.0-alpha.5 2024-03-25 18:41:44 +01:00
griffi-gh dd5af8b9e2 Prepare for publish 2024-03-25 18:35:54 +01:00
griffi-gh cf106bb893 add note about MSRV 2024-03-25 18:32:31 +01:00
griffi-gh adbc81e704 add doc_auto_cfg 2024-03-25 18:29:44 +01:00
griffi-gh 964cf22372 re-export Signal derive from the signal module 2024-03-25 18:14:36 +01:00
griffi-gh d14f27a428 document some stuff 2024-03-25 18:13:10 +01:00
griffi-gh 405963460d fix texture uv mess 2024-03-25 18:00:15 +01:00
griffi-gh dca0c0d2a4 make last rect shorter in example 7 2024-03-25 17:52:24 +01:00
griffi-gh 4e4c16ce76 use derive macro in examples 2024-03-25 17:51:34 +01:00
griffi-gh 72ff23ac0b add derive macro for Signal 2024-03-25 17:49:32 +01:00
griffi-gh 66ef58a131 use frames for progressbar 2024-03-25 16:16:56 +01:00
griffi-gh efe7326b4d load shader based on api 2024-03-25 15:37:32 +01:00
griffi-gh 55c908a3b9 disable word wrap 2024-03-25 14:26:30 +01:00
griffi-gh 6f7f3bc8b0 update readme 2024-03-25 14:26:20 +01:00
griffi-gh d8b470d805 this should fix the gap thing? 2024-03-25 14:18:14 +01:00
griffi-gh c1be9bf22b fix gap/padding layout (kinda) for remaining size 2024-03-25 14:14:12 +01:00
griffi-gh 3c6e6be754 wip Remaing size 2024-03-25 14:08:04 +01:00
griffi-gh 6da1cc5d88 more granular control over wrapping 2024-03-25 12:57:45 +01:00
griffi-gh 535a56a257 rearrange stuff 2024-03-25 12:40:06 +01:00
griffi-gh bf0b4dcdf2 add todo note :3 2024-03-25 02:46:30 +01:00
griffi-gh 807c9b087d remove fixme comment, fixed by ogl backend change 2024-03-25 02:45:43 +01:00
griffi-gh bd9c3aec81 upd slider 2024-03-25 02:31:57 +01:00
griffi-gh 579b7c5484 clean up example, make slider work 2024-03-25 02:30:51 +01:00
griffi-gh af0bf04ffc use linear minify filter 2024-03-25 02:25:46 +01:00
griffi-gh 36e6fc50ec add fixme note for tomorrow 2024-03-25 02:13:50 +01:00
griffi-gh 7daf4c44fa add note about slider 2024-03-25 02:10:15 +01:00
griffi-gh edb7305d7e aaa 2024-03-25 02:06:23 +01:00
griffi-gh 7b4772ca94 add custom slider as an example 2024-03-25 02:05:26 +01:00
griffi-gh 19ca54b1f3 add 9 patch rendering 2024-03-25 01:59:13 +01:00
griffi-gh c0af88fee8 disallow opts for rounded rects 2024-03-24 23:59:27 +01:00
griffi-gh 2db8d2f056 aaa 2024-03-24 22:55:36 +01:00
griffi-gh 95b4c47fbd prepare test 2024-03-24 22:55:15 +01:00
griffi-gh 91c9af9fd5 add image loading 2024-03-24 22:55:05 +01:00
griffi-gh 3b7059d49f allow customizing handle size 2024-03-24 22:27:59 +01:00
griffi-gh dac0c7ac6d upd api to frame 2024-03-24 22:18:15 +01:00
griffi-gh 6ec12187ac disable covers_opaque optimization for images 2024-03-24 22:01:36 +01:00
griffi-gh fc4bc83ba9 add covers_opaque 2024-03-24 21:35:44 +01:00
griffi-gh deec244547 slider: use frames (less efficient :<) 2024-03-24 21:14:24 +01:00
griffi-gh 8e7e32671c :3 2024-03-24 21:01:30 +01:00
griffi-gh 4ea98db39a update glam to 0.27 2024-03-24 21:00:46 +01:00
griffi-gh 7bd93af63f format the thingy 2024-03-24 20:58:14 +01:00
griffi-gh d4402756a3 update built-in element features 2024-03-24 20:53:03 +01:00
griffi-gh 0428af0f63 uwu 2024-03-24 19:08:24 +01:00
griffi-gh 290157e8d9 improve frame_rect macro docs 2024-03-24 19:07:51 +01:00
griffi-gh b8f9ace3da use the new macro in examples 2024-03-24 18:32:50 +01:00
griffi-gh 2a4af1aa35 add todo 2024-03-24 02:26:56 +01:00
griffi-gh 8a52fe1d67 update align_test, drop rounded_test 2024-03-24 02:24:39 +01:00
griffi-gh 7a64a6b750 make hacky macro even hackier 2024-03-24 02:19:26 +01:00
griffi-gh 9ba0a7e762 frame_rect macro 2024-03-24 02:05:28 +01:00
griffi-gh 91668e575c update most examples to new ugly api 2024-03-24 01:28:03 +01:00
griffi-gh b89a277aa9 fix fn name in ex.6 2024-03-24 01:17:26 +01:00
griffi-gh f5f3dd9ad3 remove deprecated background fn 2024-03-24 01:16:51 +01:00
griffi-gh 03a49791fa simplify with_background_frame call in example 2024-03-24 01:14:23 +01:00
griffi-gh d2598f8a20 move impls into separate module 2024-03-24 01:13:20 +01:00
griffi-gh d44a2bae53 impl from corners vec3 2024-03-24 01:10:22 +01:00
griffi-gh 3c680ea294 fix integrate frame api...
still sucks to use
2024-03-24 00:52:37 +01:00
griffi-gh 899774a7e1 document corner radius 2024-03-23 23:32:21 +01:00
griffi-gh 9acdacaa32 add docs 2024-03-23 23:23:24 +01:00
griffi-gh f8b80040f3 frame api redesign :3 2024-03-23 23:05:54 +01:00
griffi-gh fb2f3c739e remove line 2024-03-23 19:55:04 +01:00
griffi-gh b12c62e06f frame api is a mess... 2024-03-23 19:53:32 +01:00
griffi-gh 3ac83e161a rename stuff... 2024-03-23 19:42:53 +01:00
griffi-gh d1e1325068 fvck 2024-03-23 17:54:47 +01:00
griffi-gh 5ce9dda77b change name function to lower case 2024-03-23 15:54:33 +01:00
griffi-gh fa994e659a rename Br to Break 2024-03-23 15:52:54 +01:00
griffi-gh 328f745c39 add interactable active check 2024-03-23 15:52:14 +01:00
griffi-gh e22fa739c1 use new trigger api for interactable 2024-03-23 15:50:44 +01:00
griffi-gh 89037efebb add todo note 2024-03-23 15:43:22 +01:00
griffi-gh d347f8f7e9 for now, make frame non-pub 2024-03-23 15:41:19 +01:00
griffi-gh 85810a2e59 impl from corners f32 for roundedcorners 2024-03-23 15:10:32 +01:00
griffi-gh 7bfd12749b aaa 2024-03-23 15:09:05 +01:00
griffi-gh bbc1d9f1ff uhh i'm not happy with this 2024-03-23 02:05:44 +01:00
griffi-gh 915b99c3a5 frame stuff 2024-03-23 01:57:07 +01:00
griffi-gh d430996e14 impl from stuff 2024-03-22 19:35:00 +01:00
griffi-gh a0dc882799 static -> absolute, fraction -> relative 2024-03-22 18:39:18 +01:00
griffi-gh cf7919d041 add image to frame layer 2024-03-22 18:35:00 +01:00
griffi-gh 8f3e04c445 frame wip 2024-03-22 18:33:34 +01:00
griffi-gh 2af4fd900c update example 2024-03-22 15:25:01 +01:00
griffi-gh ab5998de03 slider stuff 2024-03-22 15:15:47 +01:00
griffi-gh 50ea989906 fix containers defaulting to black color 2024-03-21 23:57:35 +01:00
griffi-gh 62f22d9f12 rename ui_test_6 2024-03-21 23:53:11 +01:00
griffi-gh 8b3552407a in example 6, make the text reflect the amount 2024-03-21 23:52:49 +01:00
griffi-gh 4ad3fed6d3 add active part to slider, optimize rendering 2024-03-21 23:51:00 +01:00
griffi-gh 68114bd7dc add fn to check if completely opaque 2024-03-21 23:38:43 +01:00
griffi-gh b5f3fc192e remove deprecated BackgroundColor struct 2024-03-21 23:31:49 +01:00
griffi-gh 4574cb8862 Rename rectangle module to rect 2024-03-21 23:22:40 +01:00
griffi-gh 99527d0dcb Refactor fill color stuff 2024-03-21 23:20:47 +01:00
griffi-gh 36345119a3 Deprecate bacgroundcolor, update comments 2024-03-21 23:09:19 +01:00
griffi-gh a4cac48bda add doc comment for slider 2024-03-21 22:59:37 +01:00
griffi-gh e8d8951320 fix some warnings 2024-03-21 22:59:31 +01:00
griffi-gh 7ae72b7a41 comment out element triggers 2024-03-21 22:26:50 +01:00
griffi-gh d526928d9b a bunch of signal and input changes 2024-03-21 22:23:42 +01:00
griffi-gh dd4c71db3b compute signal in the fn body 2024-03-21 19:43:53 +01:00
griffi-gh 37345577d3 use with_title 2024-03-21 19:40:48 +01:00
griffi-gh 33d161500f slider and some input stuff 2024-03-21 18:41:28 +01:00
griffi-gh 6606119cc4 wip slider and stuff 2024-03-14 13:30:44 +01:00
griffi-gh 59a704d516 actually check event type 2024-03-13 14:35:12 +01:00
griffi-gh 03f1d75d3c stuff 2024-03-12 19:48:17 +01:00
griffi-gh 3f99151d93 remove unused deps 2024-03-12 18:37:34 +01:00
griffi-gh ad89088557 update example 5 2024-03-12 18:36:47 +01:00
griffi-gh f514746ecc use nearest filter 2024-03-12 18:19:04 +01:00
griffi-gh f47bfc1f48 uwu 2024-03-12 01:51:27 +01:00
griffi-gh cc591ffa1f drain remaining signals 2024-03-12 01:40:51 +01:00
griffi-gh a46cd7856b add br element, update docs 2024-03-12 01:38:11 +01:00
griffi-gh 7f28aebb97 somewhat less hacky... 2024-03-12 01:26:48 +01:00
griffi-gh 3a4b0eea66 well it somewhat works, build using terrible hacks 2024-03-12 01:23:26 +01:00
griffi-gh 969d0400d2 add docs 2024-03-12 01:07:17 +01:00
griffi-gh bd0364bde9 update interactable 2024-03-12 01:06:03 +01:00
griffi-gh 01b30c5979 signal stuff... 2024-03-12 00:29:26 +01:00
griffi-gh 4b3f15e6ce yeah it's a total mess 2024-03-11 21:00:43 +01:00
griffi-gh dc3f89db37 make arg mut 2024-03-11 20:52:25 +01:00
griffi-gh 7884de5560 it kinda works 2024-03-11 20:48:39 +01:00
griffi-gh dd4d48c91c fix unused imort 2024-03-11 20:17:39 +01:00
griffi-gh daeefb0684 fix warning 2024-03-11 20:16:26 +01:00
griffi-gh 35a536a0bc owo 2024-03-11 20:16:01 +01:00
griffi-gh b0df6a3ea2 uwu 2024-03-11 18:53:24 +01:00
griffi-gh eab5072d1e add some helper fns 2024-03-11 18:48:46 +01:00
griffi-gh 027bc2c429 update input handling 2024-03-11 18:40:11 +01:00
griffi-gh 67b55ec3c1 stuff 2024-03-07 23:03:13 +01:00
griffi-gh 8729e8f345 move stuff out 2024-03-07 22:46:01 +01:00
griffi-gh b348873632 fancier vscode demo 2024-03-07 02:56:05 +01:00
griffi-gh ac30a5a721 Use non-srgb textures 2024-03-07 02:47:10 +01:00
griffi-gh 32349fbb48 add wrap to vs demo 2024-03-07 02:44:04 +01:00
griffi-gh 4a1b3e4a95 vscode demo stuff 2024-03-07 02:40:47 +01:00
griffi-gh 57913757aa change stuff 2024-03-07 02:40:11 +01:00
griffi-gh 43c61cd142 add extra helpers 2024-03-07 02:24:45 +01:00
griffi-gh e297c77453 wip 2024-03-07 02:12:14 +01:00
griffi-gh 40ef524c5a Rename UiDirection to Direction 2024-03-07 02:06:14 +01:00
griffi-gh 45132db996 add Text::new 2024-03-07 02:05:39 +01:00
griffi-gh 1a6d79b2fc wip image, and required ctx stuff 2024-03-07 02:04:24 +01:00
griffi-gh 40c448276b add todo note 2024-03-07 01:19:45 +01:00
griffi-gh 2e05a30d93 add title to examples 2024-03-07 01:15:33 +01:00
griffi-gh f0bd807ff8 rename examples 2024-03-07 01:13:43 +01:00
griffi-gh 7d2cf87201 apply bg image 2024-03-07 01:12:39 +01:00
griffi-gh 806e0ce8d1 api and doc stuff, add bg image prop to container 2024-03-07 01:11:53 +01:00
griffi-gh 1c55b1217b change bg color api again 2024-03-07 00:50:47 +01:00
griffi-gh 5dc97ae2b0 set_title 2024-03-07 00:42:01 +01:00
griffi-gh b368b0ebb5 modernize mom downloader 2024-03-07 00:41:26 +01:00
griffi-gh 36de134032 clarify issue 2024-03-06 21:46:19 +01:00
griffi-gh 42d3b52272 add hack 2024-03-06 21:35:32 +01:00
griffi-gh eac62c9041 add a comment to fix stuff later 2024-03-06 21:28:35 +01:00
griffi-gh abae9d25fc use full init syntax 2024-03-06 21:28:12 +01:00
griffi-gh d4151e6c76 fix corrupted text bug 2024-03-06 21:14:40 +01:00
griffi-gh cd6d421909 fix some atlas bugs 2024-03-06 20:58:50 +01:00
griffi-gh 7a70b0ccb2 implement element wrapping 2024-03-06 20:39:25 +01:00
griffi-gh 5bf6cc5615 add settings 2024-03-06 20:34:28 +01:00
griffi-gh 758b28d235 remove non_exhaustive from Hints, upd docs 2024-03-06 17:52:34 +01:00
griffi-gh 68c0e8417a add should_wrap flag 2024-03-06 17:19:35 +01:00
griffi-gh 59271bd76d add wrap flag 2024-03-06 17:00:18 +01:00
griffi-gh fff37d7345 wrap test 2024-03-06 15:44:02 +01:00
griffi-gh c100555f4a remove comment 2024-03-05 23:47:56 +01:00
griffi-gh 48acb7ccf6 aaa 2024-03-05 23:47:40 +01:00
griffi-gh c0d6b24150 fix rounded rect gradient 2024-03-02 01:19:47 +01:00
griffi-gh 7cd716b905 fix align_test example 2024-03-02 01:09:57 +01:00
griffi-gh 9f0809c61c update docs 2024-03-02 01:07:53 +01:00
griffi-gh 663f943af3 oops 2024-03-02 00:35:00 +01:00
griffi-gh b46db55f1b ui transforms 2024-03-02 00:33:02 +01:00
griffi-gh da61904a5a fix alpha 2024-03-01 20:49:41 +01:00
griffi-gh aafb219b6b do not invalidate buffers 2024-03-01 20:49:34 +01:00
griffi-gh 52bfab6135 minor comment edit 2024-03-01 20:44:37 +01:00
griffi-gh 93f94a0786 upd log 2024-03-01 18:45:56 +01:00
griffi-gh c91e6ba50c idk a bunch of changes i forgor to commit 2024-03-01 18:21:02 +01:00
griffi-gh 0377f1e46d uwu 2024-02-29 23:53:01 +01:00
griffi-gh 99774e7f5f input handling stuff 2024-02-29 17:57:06 +01:00
griffi-gh 4745dcad1d hook up winit impl to examples 2024-02-29 16:11:29 +01:00
griffi-gh ad0e6fd7e6 refactor 2024-02-29 16:02:05 +01:00
griffi-gh 778ae751e7 stuff 2024-02-29 02:19:29 +01:00
griffi-gh 407ac29d95 input upd (wip) 2024-02-29 01:15:29 +01:00
griffi-gh c5df17b4cd
Update README.md 2024-02-28 17:16:37 +01:00
griffi-gh d7971d32ec wait it looks wrong 2024-02-28 17:01:43 +01:00
griffi-gh cdbc0559c2 update readme 2024-02-28 16:54:14 +01:00
griffi-gh 2d2e335259 upd example 2024-02-28 16:47:46 +01:00
griffi-gh 16fc3219f9 fix offset 2024-02-27 22:57:46 +01:00
griffi-gh 3938d864f1 i think this looks better... 2024-02-27 22:38:43 +01:00
griffi-gh 4d100d8fe5 cfg gate the stuff 2024-02-27 22:18:55 +01:00
griffi-gh c9611346fb add color html 2024-02-27 20:31:16 +01:00
griffi-gh becbffc367 rename stuff 2024-02-27 20:31:12 +01:00
griffi-gh 74027c52d0 add docs, update stuff 2024-02-27 18:23:55 +01:00
griffi-gh a1bfbb51d0 update size macro, add docs to layout module 2024-02-27 17:56:46 +01:00
griffi-gh 7d5869c2cb x 2024-02-26 23:49:53 +01:00
griffi-gh 0cd4e3a8a1 rename arg 2024-02-26 20:04:52 +01:00
griffi-gh b801ec8821 use derive_setters 2024-02-26 16:37:59 +01:00
griffi-gh 90f34a5dc4 api stuff 2024-02-26 16:33:55 +01:00
griffi-gh 9d7ed80be1 add checks 2024-02-26 15:19:13 +01:00
griffi-gh 30579928b0 eh... its better i guess. still needs a refactor 2024-02-26 15:13:03 +01:00
griffi-gh 371f3cb61e HACK 2024-02-26 01:30:52 +01:00
griffi-gh c8c4028bfb a 2024-02-26 01:27:25 +01:00
griffi-gh f8b2a4e15f a 2024-02-26 01:20:52 +01:00
griffi-gh 9aa61f392e wip refactor 2024-02-26 01:15:55 +01:00
griffi-gh 23dc81a921 minor changes 2024-02-25 15:59:12 +01:00
griffi-gh b064a2cb2b uwu 2024-02-25 15:43:38 +01:00
griffi-gh cdba2fedd8 kinda works now... 2024-02-25 04:02:10 +01:00
griffi-gh ae26a4d933 wip refactor 2024-02-24 23:32:09 +01:00
griffi-gh f61aa6734f x 2024-02-21 23:56:55 +01:00
griffi-gh f54b218cbb WIP single draw call architecture 2024-02-21 20:13:58 +01:00
griffi-gh 3bcbe0ae6e use usize max as the default font handle (to ensure it always stays invalid) 2024-02-21 11:20:45 +01:00
griffi-gh ff5658f44a fix builtin font handle thingy 2024-02-21 11:19:14 +01:00
griffi-gh cefa48838a bump version to 0.1.0.alpha.4 2024-02-21 11:06:13 +01:00
griffi-gh a8b065c839 remove unused enum 2024-02-21 11:03:36 +01:00
griffi-gh 9a9bb72f8d x 2024-02-20 22:45:10 +01:00
griffi-gh 9bb95b3baf publish 0.1.0-alpha.3 2024-02-20 21:21:58 +01:00
griffi-gh 73fde36c8a rename border_radius to corner_radius add it to mom downloader example 2024-02-20 21:13:21 +01:00
griffi-gh ff448ed393 New background api 2024-02-20 20:56:58 +01:00
griffi-gh bc40d4c8cc fix alpha check 2024-02-20 20:31:12 +01:00
griffi-gh 1fa292ae6b add todo note 2024-02-20 20:29:27 +01:00
griffi-gh e5859d26e0 remove unused border code 2024-02-20 20:27:05 +01:00
griffi-gh 65bd1c1b38 uwu 2024-02-20 20:24:36 +01:00
griffi-gh 92b7f6890f add border radius to progress bar 2024-02-20 20:11:05 +01:00
griffi-gh c80f42f900 Remoev option wrapper for bg color 2024-02-20 20:10:56 +01:00
griffi-gh 8393cf48a3 remove the need for wrapped option 2024-02-20 20:06:13 +01:00
griffi-gh 964ce612c2 allow unused parens 2024-02-20 20:03:39 +01:00
griffi-gh 7765d43049 remove hardcoded texture size 2024-02-20 19:59:39 +01:00
griffi-gh f02a28ee31 update stuff to new api, rename examples 2024-02-20 19:57:02 +01:00
griffi-gh d6ab6778e8 add Alignment2d, better docs 2024-02-20 19:48:32 +01:00
griffi-gh 9ff9b8356d document stuff 2024-02-20 18:19:10 +01:00
griffi-gh 8de7c4673e shorter doc 2024-02-20 17:54:53 +01:00
griffi-gh 9c97ad9cd8 another refactor :3 2024-02-20 17:49:44 +01:00
griffi-gh f657c2df5f refactor stuff 2024-02-20 17:30:26 +01:00
griffi-gh afcaf5fbef drop interactable api for now 2024-02-20 15:57:57 +01:00
griffi-gh cd50736bfb wip winit impl 2024-02-20 01:00:57 +01:00
griffi-gh e9cfd19c01 restructure stuff, add builtin_container feature 2024-02-19 23:13:35 +01:00
griffi-gh 250e821396 add pixel_perfect feature, document stuff 2024-02-19 21:32:13 +01:00
griffi-gh e3c4be7fdc refactor draw stuff 2024-02-19 21:12:12 +01:00
griffi-gh 52d8b9866f update rounded corner api 2024-02-19 19:40:18 +01:00
griffi-gh a1279f8193 update metadata 2024-02-19 17:49:01 +01:00
griffi-gh 92dfb32e1d upd readme 2024-02-19 17:47:43 +01:00
griffi-gh 95535c0292 upd readme 2024-02-19 17:35:12 +01:00
griffi-gh cc3f397c69 bump version 2024-02-19 17:27:58 +01:00
griffi-gh 23ca81670f update readme 2024-02-19 17:27:16 +01:00
griffi-gh 0b5d51603e fix readme 2024-02-19 17:25:12 +01:00
griffi-gh 5fffb921b5 create hui-winit 2024-02-19 17:24:31 +01:00
griffi-gh 1d3f44d6c4 use url assets to make the readme work for crates 2024-02-19 17:13:06 +01:00
griffi-gh 5f6eb18b0f update readme 2024-02-19 17:10:30 +01:00
griffi-gh 5290abc41f remove unused clip option 2024-02-19 14:44:01 +01:00
griffi-gh 3432550566 update mom downloader example 2024-02-19 14:26:59 +01:00
griffi-gh b2a318a0f6 publish version 0.1.0-alpha.2 2024-02-19 14:03:30 +01:00
griffi-gh 73316e630d update rounded example 2024-02-19 14:01:51 +01:00
griffi-gh da58e8112f update example 2024-02-19 13:57:39 +01:00
griffi-gh a4c06ac61d fix center alignment with padding 2024-02-19 13:57:33 +01:00
griffi-gh b59447f644 upd example 2024-02-19 13:46:13 +01:00
griffi-gh 36947bff94 kinda.. works? 2024-02-19 05:50:46 +01:00
griffi-gh ec3ebeb4b8 kinda works 2024-02-19 05:46:43 +01:00
griffi-gh fa5b89b3ec corners 2024-02-19 05:36:38 +01:00
griffi-gh f2ff2ccfb5 wip 2024-02-19 04:37:28 +01:00
griffi-gh 6db5267cef wip rounded corners api 2024-02-19 03:41:48 +01:00
griffi-gh a3a133ec12 rename stuff 2024-02-18 19:27:45 +01:00
griffi-gh f24bff2755 fix most warnings 2024-02-18 17:22:31 +01:00
griffi-gh 41049e5f59 change version track to 0.1.0-alpha.x 2024-02-18 04:06:24 +01:00
griffi-gh e0ee7c9aa6 new "context" system and text measuring 2024-02-18 04:04:02 +01:00
griffi-gh 1e1dccde0c Merge branch 'master' of https://github.com/griffi-gh/hui 2024-02-17 23:06:13 +01:00
griffi-gh 7900b6bb91
Update README.md 2024-02-17 22:56:07 +01:00
griffi-gh 487112c4aa
Update README.md 2024-02-17 22:53:39 +01:00
griffi-gh 8e4e9827f7
Update README.md 2024-02-17 22:49:45 +01:00
griffi-gh f69ebe2bd5
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"]
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");
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();
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()
},
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();
run: |ui, max_size, instant| {
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
})
.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 {
window_target.exit();
std::process::exit(0);
}
}),
..Default::default()
})],
..Default::default()
}, resolution);
hui.end();
backend.update(&hui);
backend.draw(&mut frame, resolution);
frame.finish().unwrap();
})
.add_child(ui);
})
.add_root(ui, max_size)
}
_ => (),
}
}).unwrap();
}

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();
if self.vertex_count == 0 || self.index_count == 0 {
self.vertex_buffer.invalidate();
self.index_buffer.invalidate();
if self.vertex_count == 0 || self.index_count == 0 {
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() {
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[..];
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,
}
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(),
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,
//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);
}
impl CallSwapper {
pub fn new() -> Self {
Self {
calls: vec![],
call: UiDrawCall::default(),
}
}
pub fn current(&self) -> &UiDrawCall {
&self.call
}
//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
pub fn current_mut(&mut self) -> &mut UiDrawCall {
&mut self.call
}
atlas.lock_atlas = true;
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 {
match command {
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::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");
let do_swap = if let Some(prev_command) = prev_command {
std::mem::discriminant(prev_command) != std::mem::discriminant(command)
//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 {
false
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,
});
};
if do_swap {
swapper.swap();
}
//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,
);
if do_swap || prev_command.is_none() {
match command {
UiDrawCommand::Rectangle { .. } => (),
UiDrawCommand::Text { .. } => {
swapper.current_mut().bind_texture = Some(BindTexture::FontTexture);
// 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,
]);
}
}
}
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([
//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,
uv: vec2(0.0, 0.0),
color: color.top_left,
uv: uvs.top_left,
},
UiVertex {
position: *position + vec2(size.x, 0.0),
color: *color,
uv: vec2(1.0, 0.0),
color: color.top_right,
uv: uvs.top_right,
},
UiVertex {
position: *position + *size,
color: *color,
uv: vec2(1.0, 1.0),
color: color.bottom_right,
uv: uvs.bottom_right,
},
UiVertex {
position: *position + vec2(0.0, size.y),
color: *color,
uv: vec2(0.0, 1.0),
color: color.bottom_left,
uv: uvs.bottom_left,
},
]);
}
},
UiDrawCommand::Text { position, size, color, text, font } => {
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) {
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;
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,
},
UiDirection::Vertical => {
size.x = size.x.max(measure.size.x);
size.y += measure.size.y + self.gap;
leftover_gap.y = self.gap;
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 -= leftover_gap;
};
let inner_content_size = Some(size);
//size of AABB containing all lines
let mut total_size = Vec2::ZERO;
size += vec2(
//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;
//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,
},
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);
}
}
}
line_size -= leftover_gap;
//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) {
//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),
};
//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.),
// };
// }
// }
for (line_idx, cur_line) in user_data.lines.iter().enumerate() {
let mut local_position = position;
//alignment on primary axis
match (pri_sec_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, Direction::Horizontal) => {
local_position.x += (ctx.measure.size.x - cur_line.content_size.x) / 2. - self.padding.left;
},
(Alignment::Center, UiDirection::Vertical) => {
position.y += (measure.size.y - measure.hints.inner_content_size.unwrap().y) / 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) => {
position.x += measure.size.x - measure.hints.inner_content_size.unwrap().x - self.padding.right - self.padding.left;
(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) => {
position.y += measure.size.y - measure.hints.inner_content_size.unwrap().y - self.padding.bottom - self.padding.top;
(Alignment::End, Direction::Vertical) => {
local_position.y += ctx.measure.size.y - cur_line.content_size.y - self.padding.bottom - self.padding.top;
}
}
for element in &self.elements {
//(passing max size from layout rather than actual bounds for the sake of consistency with measure() above)
let next_line_begin = user_data.lines
.get(line_idx + 1)
.map(|l| l.start_idx)
.unwrap_or(self.children.0.len());
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,
max_size: self.measure_max_inner_size(layout),
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(state, &el_layout);
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)
match (self.align.1, self.direction) {
//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, UiDirection::Horizontal) => {
el_layout.position.y += (measure.size.y - self.padding.bottom - self.padding.top - el_measure.size.y) / 2.;
(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, UiDirection::Vertical) => {
el_layout.position.x += (measure.size.x - self.padding.left - self.padding.right - el_measure.size.x) / 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.;
},
(Alignment::End, UiDirection::Horizontal) => {
el_layout.position.y += measure.size.y - el_measure.size.y - self.padding.bottom;
//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, UiDirection::Vertical) => {
el_layout.position.x += measure.size.x - el_measure.size.x - self.padding.right;
(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(&el_measure, state, &el_layout, draw);
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 {
UiDirection::Horizontal => {
position.x += el_measure.size.x + self.gap;
Direction::Horizontal => {
local_position.x += el_measure.size.x + self.gap;
},
UiDirection::Vertical => {
position.y += el_measure.size.y + self.gap;
Direction::Vertical => {
local_position.y += el_measure.size.y + self.gap;
}
}
}
//Move to the next line
match self.direction {
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 measure(&self, _: &StateRepo, layout: &LayoutInfo) -> 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,
fn name(&self) -> &'static str {
"progress_bar"
}
),
fn measure(&self, ctx: MeasureContext) -> Response {
Response {
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()
}
}