Compare commits

...

248 Commits
master ... c3

Author SHA1 Message Date
1905191453 fix wrong id in scrollbar 2025-10-28 22:00:29 +01:00
8e39eee4af fix scolling for nested divs 2025-10-28 21:11:45 +01:00
da001601e5 fix wrong sprite scale 2025-10-28 00:26:37 +01:00
665e10fa30 fix absolute placement and scrollbar 2025-10-28 00:26:18 +01:00
66acf8d4a3 skip frame on resize 2025-10-27 20:03:24 +01:00
6a88ea55ec add license 2025-10-27 13:00:34 +01:00
3ac556b541 fix negative grow element size 2025-10-27 12:53:13 +01:00
1ec6eb88c9 implemented @popup() macro 2025-10-27 11:02:11 +01:00
49666294dc fixed @compute_id() to accept non-constants 2025-10-27 11:01:50 +01:00
18a86e8aab vertical and horizontal lines, @row and @column return their ids like @div 2025-10-26 17:48:42 +01:00
4f7fa7d50c major project restructure 2025-10-25 17:32:41 +02:00
bd31f562fc moved renderer to submodule 2025-10-25 17:11:00 +02:00
7b036ccee2 moved renderer to it's own library and module 2025-10-25 15:13:06 +02:00
26f38b342b fix weird input behavior 2025-10-25 15:11:55 +02:00
a8b7171709 enable 01 and parallel compilation 2025-10-23 22:31:58 +02:00
ed2b36ef0f better highlight color 2025-10-23 22:31:32 +02:00
bb6a166f2a reset more input fields on frame end 2025-10-23 22:31:03 +02:00
793fd1aa28 implement sprite scaling 2025-10-23 22:30:18 +02:00
c63d08c462 add @inline to font_load to fix segfault 2025-10-22 15:00:43 +02:00
a677d3f1f0 removed dependency on libgrapheme 2025-10-22 14:56:47 +02:00
7ff787f71f separate font module and font now allocates in arena 2025-10-22 14:35:35 +02:00
be51e37231 home and end 2025-10-20 23:37:36 +02:00
fe6f32c769 move around with ctrl 2025-10-20 16:15:57 +02:00
a512fe6c71 fix selection edge cases 2025-10-19 22:53:57 +02:00
2eec1fb710 better text edit 2025-10-19 21:10:51 +02:00
546f3628c7 semi-working text 2025-10-18 17:40:27 +02:00
ce9d1e6684 string layout with custom iterator 2025-10-16 22:38:56 +02:00
05a6d4803e move cursor with mouse 2025-10-14 10:44:13 +02:00
e3c0bac9ca rewrote string layout 2025-10-13 23:55:41 +02:00
5dfbad2399 notes 2025-10-13 00:26:36 +02:00
3b66e51cc6 Merge branch 'c3' of https://git.alemauri.eu/alema/ugui into c3 2025-10-12 13:22:06 +02:00
eb62e9ad72 Merge branch 'c3' of https://git.alemauri.eu/alema/ugui into c3 2025-10-12 13:19:52 +02:00
34d078b524 optional alpha channel in color properties 2025-10-12 13:19:49 +02:00
b9e91c3119 implemented a frame skip request 2025-10-11 22:32:00 +02:00
b5ef86d092 actually correct input handling 2025-10-10 22:31:28 +02:00
fb3a964f7f first draft 2025-10-08 22:16:17 +02:00
884105a4e2 faster sort of command queue 2025-10-07 17:47:24 +02:00
89f19ccf2e Update README.md 2025-10-06 23:46:27 +02:00
bbfc306984 sort cmd queue 2025-10-06 23:23:35 +02:00
6b8083ab5c remove fifo from imports 2025-10-05 23:14:16 +02:00
2bb907d523 use std::collections::list instead of custom fifo 2025-10-05 01:24:56 +02:00
d7cab085f7 removed vtree, contracts on mtree 2025-10-04 19:58:34 +02:00
01c2fa3367 contracts in font.c3 2025-10-04 19:42:59 +02:00
167676f478 use $feature() to enable debug features 2025-10-03 15:31:44 +02:00
8cecb57d93 use a tuple in get_elem 2025-10-03 15:19:52 +02:00
32a57d5293 popups 2025-10-02 23:19:42 +02:00
e7cfa3517f removed ugui_prefix in filenames 2025-09-30 22:27:59 +02:00
1f66c23919 use @row and @column macros 2025-09-30 22:19:14 +02:00
a9642f28bd add convenience function to draw a semi-transparent rectangle 2025-09-30 22:17:08 +02:00
6a7dd998a6 offset first character of a line to make alignment prettier 2025-09-30 22:16:42 +02:00
225f61079d Merge branch 'c3' of https://git.alemauri.eu/alema/ugui into c3 2025-09-30 21:39:36 +02:00
76cef2caa0 added @row and @column macros 2025-09-30 21:39:00 +02:00
96fda0c5e9 re-implemented scrollbars 2025-09-29 23:51:02 +02:00
b99229b48d fix absoulte grow children 2025-09-29 23:24:19 +02:00
d47b835020 fix cursor placement 2025-09-27 12:43:38 +02:00
f1b6321d3d optional anchor in text_box 2025-09-27 12:43:21 +02:00
63b3d05b19 up and down in textedit 2025-09-25 23:16:18 +02:00
24216e4ab4 add separator widget 2025-09-24 22:49:51 +02:00
6a13245fd9 revert ccca2be49684cdcc905d1acc0b8d2c4cb126c4b4 2025-09-24 22:49:34 +02:00
fe9e2bdf49 moved test_renderer.c3 2025-09-23 23:32:34 +02:00
ccca2be496 "fixed" segfault with >00
idk if this is a compiler error but leaving the ascii pre-caching in results
in the pointer to the font structure to be corrupted before font.get_glyph()
2025-09-23 23:30:22 +02:00
6839a7e06c documentation of mtree 2025-09-23 23:30:11 +02:00
7f8b5196a5 new tree implementation
this about halves the time spent on level_order_it and drastically reduces the
time spent in children_it
2025-09-21 18:17:39 +02:00
c046c6af52 some comments 2025-09-20 00:27:24 +02:00
df18be7bf6 bools for VSYNC and FPS_LIMIT 2025-09-17 22:49:46 +02:00
48d1b29537 first draft for absolute positioning 2025-09-17 22:45:03 +02:00
915f395b5a corrected handling of newline in layout_string() 2025-09-16 17:06:19 +02:00
be1476d107 renderer now uses a single pipeline for ugui 2025-09-15 18:53:42 +02:00
622b648d26 corrected layout offset 2025-09-14 20:32:31 +02:00
d33d72a074 fixed check_key_combo 2025-09-13 20:02:21 +02:00
34b92c93b4 re-implemented text box
also includes
	- small layout fix for grow elements
	- ElemEvents now includes has_focus flag
2025-09-13 19:53:50 +02:00
81cc3dae65 div_end returns the div Id 2025-09-12 22:45:23 +02:00
d35ef7ddaf re-implemented toggles 2025-09-12 22:44:33 +02:00
48a333e501 simplified code 2025-09-12 22:18:15 +02:00
71a959b9a1 re-added sliders 2025-09-12 20:23:07 +02:00
a00e39f36b Merge branch 'c3' of https://git.alemauri.eu/alema/ugui into c3 2025-09-12 12:52:31 +02:00
e328a67d96 re-implemented checkbox 2025-09-12 12:48:29 +02:00
be951c616a fix dimensions being calculated wrong 2025-09-12 12:47:00 +02:00
636f162b10 fixed problem with >31 elements 2025-09-12 11:44:26 +02:00
2bd15ac981 Merge branch 'c3' of https://git.alemauri.eu/alema/ugui into c3 2025-09-11 19:00:46 +02:00
f8befeea4d idk 2025-09-11 19:00:41 +02:00
9d96d2eb74 moved widgets to their own folder 2025-09-09 20:01:10 +02:00
c3a6390404 draw text correctly 2025-09-09 19:10:04 +02:00
db63b2c6b1 layout actually works now 2025-09-08 23:43:59 +02:00
869c7871f9 minor changes 2025-09-06 12:50:36 +02:00
3d7be2a2df implement operator overloading for rects 2025-09-05 19:56:59 +02:00
335624fcbe in layout use a combined version of margin, border and padding 2025-09-05 18:53:47 +02:00
0f7d5a6506 working example of the new layout system 2025-09-05 13:10:16 +02:00
2619873ca7 tested new layout system 2025-09-03 23:32:04 +02:00
24ac28e0d9 update sdl3.c3l 2025-08-29 19:25:18 +02:00
5e5c912092 use the specified allocator for the element caches 2025-08-29 19:24:52 +02:00
4a690fdeb5 actually useful calculator example 2025-08-20 17:33:25 +02:00
62ebd6592d fix wrong scissor 2025-08-20 17:32:45 +02:00
00299bec0b fix calculator demo
turns out it was an incorrect handling of  scissor test
2025-08-16 10:10:06 +02:00
be00c87c6a implement a convenient macro to start and end a div 2025-08-14 22:16:54 +02:00
e8bb35811a renamed position_element() to layout_element() 2025-07-14 13:16:04 +02:00
278e4988e9 use containing_rect() in position_element() 2025-07-14 13:10:14 +02:00
78fc1c1e87 cleaner get_parent() 2025-07-14 13:05:30 +02:00
8d79f13fd6 added another demo ui 2025-07-14 12:59:07 +02:00
7713cd7da9 prettier radius 2025-07-14 12:58:15 +02:00
00aa01109e crash if last element is not root 2025-07-14 12:57:53 +02:00
5e68671828 fixed divs-in-divs 2025-07-14 12:57:29 +02:00
8367f6b617 better css lexer 2025-07-13 21:43:57 +02:00
dd073385c8 fix slider styling 2025-07-13 20:12:48 +02:00
80d17d7b33 unified button element 2025-07-13 20:08:18 +02:00
b48c413d2d changed text (glyph) placement 2025-07-10 11:05:39 +02:00
c1c1247af4 update TODO 2025-07-07 11:46:40 +02:00
5ae9b05223 moved style to style.css 2025-07-07 11:37:56 +02:00
9afb0d2acd better default style handling 2025-07-06 23:50:36 +02:00
c1bf8e891b fixed regression in scrollbars in divs 2025-07-06 01:41:57 +02:00
777e974841 use new style system 2025-07-05 16:37:08 +02:00
9fc1d90455 simple style import with a subset of css 2025-07-04 11:17:42 +02:00
a390a8908c changed Id to uint since builtin hash functions hash to uints 2025-07-01 16:48:30 +02:00
c49a689304 get_elem now also pushes into the tree and check the correct type 2025-07-01 16:03:11 +02:00
849b267f91 renamed get_element_by_tree_idx to get_active_div 2025-07-01 15:33:22 +02:00
1de4fd78b8 better checkboxes 2025-07-01 15:28:08 +02:00
586e81935c improved the api to not require explicit labels everywhere 2025-06-30 18:24:50 +02:00
972c9b581d text input box 2025-06-30 13:10:00 +02:00
6d2594db2d test id generation with macros 2025-06-30 13:08:52 +02:00
9827a899f1 get_line_height and get_cursor_position 2025-06-30 13:08:27 +02:00
88d9b65028 updated build configuration 2025-06-30 13:07:42 +02:00
c98c00bfb9 ecode editor config 2025-06-25 10:47:44 +02:00
fc3fa32ddd more smoothing on rounded corners 2025-06-25 10:47:23 +02:00
cd83c528ee fixed vulkan validation errors 2025-06-19 15:24:46 +02:00
5b0169cd94 better input handling 2025-06-17 18:23:36 +02:00
b411718c94 add fps counter 2025-06-15 23:47:09 +02:00
05232c1d24 do a single upload pass to reduce badwidth 2025-06-15 23:35:37 +02:00
0223536ac8 Merge branch 'c3-instanced' into c3 2025-06-15 20:54:57 +02:00
f30db0dd47 implemented instanced rendering 2025-06-15 18:54:35 +02:00
865c7dabaa indirect rendering 2025-06-15 00:26:27 +02:00
0ef2bceeec handle scissor and vsync 2025-06-14 15:09:34 +02:00
cda2f27a49 Batch uploads to the gpu 2025-06-14 14:49:53 +02:00
10ee643a0c disable vsync 2025-06-12 20:01:33 +02:00
00f5e71666 enable cycling when mapping the transfer buffers to avoid corruption 2025-06-12 19:56:43 +02:00
458c45d2b9 using the new renderer 2025-06-12 19:49:43 +02:00
c4a3dd3a26 tweak ugui api, add some usefult functions 2025-06-12 18:56:57 +02:00
2014c67bfd tweak renderer API 2025-06-12 18:56:20 +02:00
177e52b0d0 rounded quads baby! 2025-06-10 12:54:03 +02:00
3a0904023a updated sdl3.c3l location 2025-06-10 12:53:52 +02:00
c4c9716d61 remove mqoi dependency in ugui manifest 2025-06-09 16:35:23 +02:00
6208711292 use the language's qoi decoder 2025-06-09 16:06:49 +02:00
39bd7fb8bc removed module raylib.c3l (non vendor) 2025-06-09 15:51:16 +02:00
47eb3ff907 moved vendor libraries to lib/ as a submodule 2025-06-09 15:50:29 +02:00
21aa70d340 create binary directory before copying 2025-06-09 15:10:38 +02:00
bd8c73ecd5 sdl3.c3l as a submodule 2025-06-09 12:30:25 +02:00
6e65700f38 scary quads and nice sprites 2025-06-07 12:42:57 +02:00
e3d87525d4 draw multiple quads 2025-06-07 10:35:08 +02:00
3002123ef7 removed bad error handling and replaced it with worse error handling 2025-06-03 22:36:18 +02:00
ac3fcae649 sorting the command buffer 2025-06-03 22:03:14 +02:00
c9b74aebc7 implement z index in command buffer 2025-06-03 18:15:46 +02:00
f344c989db test different draw calls for one render pass (failed) 2025-06-03 18:15:33 +02:00
6c5acd6f23 enable foreach for FIFO 2025-06-03 18:15:12 +02:00
24bc2c67bc changed the pipeline to use 16 bit int as coords
thank you renderdoc for making me feel less stupid
2025-06-03 09:16:51 +02:00
712ce50631 A lot of work
* moved all ugui code to lib/ugui.c3l and made it a library/module
* started work on a sdl3 renderer, with shaders etc
* added the new sdl3.c3l library as a dependency
* makefile is for the renderer
2025-06-01 16:44:31 +02:00
2380c7693c add sdl3 dependency 2025-05-21 23:38:59 +02:00
79a2d66880 update project to c3 0.7.1 2025-05-05 16:23:26 +02:00
34e75f8c06 larger font cache 2025-02-08 12:51:10 +01:00
7c6f7d31d2 quick and dirty checkbox 2025-02-07 23:46:21 +01:00
52f3929a42 renamed ATlAS_RGBA32 to ATLAS_R8G8B8A8 2025-02-06 23:51:15 +01:00
e09107af98 simpler main 2025-02-06 23:44:03 +01:00
588a417413 checkbox and msdf sprite rendering 2025-02-04 22:40:44 +01:00
14359a9b7e first checkbox 2025-02-03 23:07:32 +01:00
196a2474fd todos 2025-02-03 23:07:14 +01:00
c53b9eed5e MAYBE correct layout with next_row and next_column 2025-02-01 01:01:13 +01:00
92614e4d8b removed comments 2025-01-31 23:15:40 +01:00
d94e430807 added library submodules 2025-01-31 12:37:15 +01:00
de64746fdf renamed libraries 2025-01-31 12:20:19 +01:00
0531f58a56 less cached elements by default 2025-01-30 22:27:46 +01:00
f516a68cee correct font atlas size 2025-01-30 22:27:24 +01:00
07857fcd44 switch to c3s' vendor raylib 5.5 2025-01-30 19:38:53 +01:00
fbe631b4b4 better sprites 2025-01-30 18:36:47 +01:00
b317951c32 first working prototype of sprite drawing 2025-01-29 01:10:18 +01:00
9aa0d58d68 better to_rgba() macro 2024-12-28 16:59:12 +01:00
78e2c64da6 implemented adaptive size divs 2024-12-26 22:58:43 +01:00
16adfd7cc5 idk look at the changes 2024-12-25 12:30:35 +01:00
1746f7d940 update TODO 2024-12-23 15:56:40 +01:00
169b5e1dfd cull commands that result in zero-area bounding boxes 2024-12-23 15:49:46 +01:00
87de68028a update todo 2024-12-20 20:58:12 +01:00
04843fe714 do not use xor to combine keys since when more than two layers deep this would result in identical keys 2024-12-20 20:17:53 +01:00
a0c6a3b2cb work on button with label 2024-12-20 19:50:58 +01:00
ca691d1294 enlarge scrollbars when focused 2024-12-20 02:17:41 +01:00
f7985f8c7f track focused and hovered elements 2024-12-20 01:45:10 +01:00
4bd827ce5c set ids <facepalm> 2024-12-19 19:42:02 +01:00
d31b4eab53 update todo 2024-12-19 15:27:14 +01:00
1088083e1e stuff, mostly renaming variables 2024-12-19 15:24:39 +01:00
601a396aa8 scrollbar fixup 2024-12-19 00:29:30 +01:00
a481269022 work on div sliders, major changes
* Ids are now keyed based on the parent's id, this means that an element can have
  the same label when placed in different divs
* Divs now enable the scissor test, this way the elements cannot draw outside of
  the parent div bounds
* Introduced a LAYOUT_ABSOLUTE that disables all layout logic, for internal use
* Divs now draw scrollbars using the slider_hor and slider_ver elements
2024-12-18 20:04:23 +01:00
499f6dc79b removed force update 2024-12-18 15:10:17 +01:00
740ea0c6be merged ugui_impl and ugui_data to ugui_core 2024-12-18 15:02:46 +01:00
8d4b353e88 a lot of work on sliders 2024-12-18 14:58:40 +01:00
c0e9565bf6 idk some stuff 2024-12-17 11:26:59 +01:00
c1a7b4fcdb display timing statistics 2024-12-16 17:06:46 +01:00
7d9a8a1363 pre-cache ascii range in font atlas 2024-12-16 17:06:33 +01:00
2e0c6333d3 draw border around floating divs 2024-12-16 17:06:16 +01:00
3a7655a3f0 specify texture id in the sprite command 2024-12-16 17:05:44 +01:00
7b7aac8df4 schrift use ZString where necessary 2024-12-16 14:08:08 +01:00
2e60e4c5b8 fix font alpha channel 2024-12-16 14:07:44 +01:00
bca29c537c scissor command 2024-12-15 22:29:07 +01:00
6d8300f9d9 better font atlas implementation 2024-12-15 21:39:26 +01:00
5a89e9ec7d fixed formatting fuckups offered by the zed team 2024-12-14 13:29:45 +01:00
0db858e814 use libgrapheme to interpret utf8 encoded strings 2024-12-14 01:12:54 +01:00
8cf3881b6b added libgrapheme 2024-12-13 16:44:07 +01:00
373243d138 fix rect roundness 2024-12-13 14:42:15 +01:00
3070fac9f5 cull zero area rects 2024-12-13 14:41:54 +01:00
5c687bd24e add push_sprite() 2024-12-13 14:05:07 +01:00
c880c2b26e correct text bounds 2024-12-13 13:56:02 +01:00
089140e1ed substitute enqueue() with push_rect() 2024-12-13 13:51:23 +01:00
fb177c03f7 some todos 2024-12-12 15:46:40 +01:00
328cac871a started work on border radius 2024-12-12 15:46:24 +01:00
f0aa59ef0b even better text rendering 2024-12-11 22:25:53 +01:00
61556d0a2c forgot the baseline 2024-12-11 20:39:06 +01:00
2356d165fe somewhat functional text rendering 2024-12-11 01:14:14 +01:00
dbe70eb4f4 added library schrift 2024-12-06 22:03:35 +01:00
f86a360f39 correct slider handle placement 2024-12-02 18:48:30 +01:00
7e18c7a316 only reset div elements to default if new element 2024-12-01 00:28:08 +01:00
537acd4765 added a way to force a relayout 2024-12-01 00:26:56 +01:00
d5bea68058 report timings 2024-12-01 00:25:43 +01:00
574a1f23dc some tests 2024-11-21 00:50:42 +01:00
f8e2c0b70c more TODO 2024-11-21 00:50:25 +01:00
9a785e0f06 use builtin hash functions instead of rolling my own 2024-11-21 00:50:11 +01:00
bb1745a05d use map::HashMap instead of map::Map and use clz() to find a free spot 2024-11-21 00:46:27 +01:00
04dff26067 reduced maximum elements to 1024 2024-11-21 00:45:50 +01:00
fa3362cc66 wait for events to reduce cpu usage 2024-11-20 17:12:01 +01:00
73bc933eb5 semi-working vertical slider 2024-11-17 23:36:12 +01:00
763e9ba8d6 correct placement and box model 2024-11-14 23:42:20 +01:00
250a0fb3b5 initial work on scrollable divs 2024-11-07 18:35:20 +01:00
8bc38452b3 renamed elem.rect to elem.bounds 2024-11-02 09:44:53 +01:00
1cad13e597 vertical slider 2024-11-02 09:41:54 +01:00
28598f0575 slider 2024-10-31 13:04:30 +01:00
f48151b38e initial refactor 2024-10-31 00:04:49 +01:00
39e78ea078 ported rewrite2 to c3 2024-10-29 22:45:47 +01:00
2dcc1b582c started working on events 2024-03-17 23:34:45 +01:00
a374a37971 mouse buttons 2024-03-15 23:30:02 +01:00
5427b191c2 mouse input 2024-03-15 22:49:07 +01:00
3a8a55d177 checkpoint 2024-03-10 17:26:03 +01:00
a4974c8df8 fm: initial commit 2024-03-05 14:13:41 +01:00
97295df516 some style 2024-01-12 21:33:14 +01:00
305df93182 more layouting 2024-01-12 19:59:04 +01:00
d6358944ac some layout work 2024-01-11 18:53:20 +01:00
59acce1150 Merge branch 'rewrite2' of https://git.alemauri.eu/alema/ugui into rewrite2 2024-01-09 16:09:13 +01:00
d4c97e1f4f fifo 2024-01-09 16:05:51 +01:00
28b5ee16fb mar 9 gen 2024, 16:00:03, CET 2024-01-09 16:00:03 +01:00
7909306d7a layout ideas 2024-01-02 01:34:57 +01:00
99df8ad38d fixed tree_prune and level_order_it 2024-01-01 22:49:03 +01:00
156c3b3959 less than bare minimum 2024-01-01 20:43:19 +01:00
94837ed410 things 2023-12-29 12:44:53 +01:00
4aefe8b42d tree in a vector 2023-12-27 17:36:30 +01:00
71080476b1 just a tree 2023-11-02 11:25:53 +01:00
80 changed files with 5209 additions and 23052 deletions

11
.gitignore vendored
View File

@ -1,5 +1,8 @@
microgui
*.o
test/test
**/compile_commands.json
**/.cache
*.a
build/*
**/.ccls-cache
perf.data*
*.rdc
test_renderer
resources/shaders/compiled/**

0
.gitmodules vendored Normal file
View File

243
LAYOUT Normal file
View File

@ -0,0 +1,243 @@
Div Children Alignment
+------------------------------------------------+
|TOP-LEFT TOP TOP-RIGHT|
| |
| |
| |
| |
| |
| |
|LEFT CENTER RIGHT|
| |
| |
| |
| |
| |
| |
|BOTTOM-LEFT BOTTOM BOTTOM-RIGHT|
+------------------------------------------------+
ALIGNMENT CHART:
+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
| TOP-LEFT, ROW: | TOP-LEFT, COLUMN: | BOTTOM, ROW: | BOTTOM, COLUMN: | TOP-RIGHT, ROW: | TOP-RIGHT, COLUMN: |
| | | | | | |
| +------------------------- - | +----------- - | | +----+ | | - -----------+ |
| |+-------++-----++-----+ | |+-------+ | | | E1 | | | +-------+| |
| || E1 || E2 || | | || E1 | | | | | | - -----------------------+ | | E1 || |
| || |+-----+| E3 | | || | | | +----+ | +-------++-----++-----+| | | || |
| |+-------+ | | | |+-------+ | +-------+ +---+ | +------+ | | E1 || E2 || || | +-------+| |
| | +-----+ | |+----+ | | E1 |+------+|E3 | | | E2 | | | |+-----+| E3 || | +----+| |
| ' | || E2 | | | || E2 || | | | | | +-------+ | || | | E2 || |
| ' | |+----+ | +-------++------++---+ | +------+ | +-----+| | +----+| |
| | |+---------+ | - ------------------------ - | +--+ | ' | +---------+| |
| | || E3 | | | |E3| | ' | | E3 || |
| | |+---------+ | | +--+ | | +---------+| |
| | ' | | - ----------- - | | ' |
| | ' | | | | ' |
| | | | | | |
+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
| LEFT, ROW: | LEFT, COLUMN: | BOTTOM-RIGHT, ROW: | BOTTOM-RIGHT, COLUMN: | TOP, ROW: | TOP, COLUMN: |
| | | | | | |
| | ' | | ' | | - -------------- - |
| ' | |+-------+ | | +-------+| | | +----------+ |
| | +----+ | || E1 | | ' | | E1 || | - ----------------------- - | | E1 | |
| |+------+ | | | || | | +-----+| | | || | +------++----++-----+ | | | |
| || |+-----+| | | |+-------+ | +-------+ | || | +-------+| | | E1 || E2 || E3 | | +----------+ |
| || E1 || E2 || E3 | | |+----+ | | E1 |+-----+| E3 || | +----+| | | |+----+| | | +--------+ |
| || |+-----+| | | || E2 | | | || E2 || || | | E2 || | +------+ | | | | E2 | |
| |+------+ | | | |+----+ | +-------++-----++-----+| | +----+| | +-----+ | +--------+ |
| | +----+ | |+---------+ | - -----------------------+ | +---------+| | | +------+ |
| ' | || E3 | | | | E3 || | | | E3 | |
| ' | |+---------+ | | +---------+| | | | | |
| | ' | | - -----------+ | | +------+ |
| | ' | | | | |
+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
| BOTTOM-LEFT, ROW: | BOTTOM-LEFT, COLUMN: | RIGHT, ROW: | RIGHT, COLUMN: | CENTER, ROW: | CENTER, COLUMN: |
| | | | | | |
| | ' | | ' | | | |
| | |+-------+ | | +-------+| | | | +-----------+ |
| | || E1 | | ' | | E1 || | | | | E1 | |
| ' | || | | +----+| | | || | | +----+ | | | | |
| | +-----+ | |+-------+ | +------+ | || | +-------+| | +------+ | | | | +-----------+ |
| |+-------+ | | | |+----+ | | |+-----+| || | +----+| | | |+----+| | | +---------+ |
| || E1 |+-----+| E3 | | || E2 | | | E1 || E2 || E3 || | | E2 || | ---|--E1--||-E2-||-E3-|--- | ----|---E2----|---- |
| || || E2 || | | |+----+ | | |+-----+| || | +----+| | | |+----+| | | +---------+ |
| |+-------++-----++-----+ | |+---------+ | +------+ | || | +---------+| | +------+ | | | | +-------+ |
| +------------------------- - | || E3 | | +----+| | | E3 || | | +----+ | | E3 | |
| | |+---------+ | ' | +---------+| | | | | | | |
| | +----------- - | ' | ' | | | +-------+ |
| | | | ' | | | |
| | | | | | |
+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
div (
align: TOP-LEFT | LEFT | BOTTOM-LEFT | BOTTOM | BOTTOM-RIGHT | RIGHT | TOP-RIGHT | RIGHT | CENTER
size_x/y: EXACT(x) | GROW() | FIT(min, max)
scroll_x/y: true | false
resize_x/y: true | false
layout: ROW | COLUMN
)
align: alignment of the children elements
size: how the div should be sized
scroll: enables scrollbars
layout: the layout direction of the children
COLUMN ROW
+--------------------+ +----------------------------------------------------+
| +----------------+ | |+----------------+ |
| | | | || |+------------+ |
| | | | || || |+------------------+|
| | E1 | | || E1 || E2 || E3 ||
| | | | || || |+------------------+|
| | | | || |+------------+ |
| +----------------+ | |+----------------+ |
| +------------+ | +----------------------------------------------------+
| | | |
| | E2 | |
| | | | (both have center alignment)
| +------------+ |
|+------------------+|
|| ||
|| E3 ||
|| ||
|| ||
|+------------------+|
+--------------------+
Element {
id: uint
sizing: { min_w, min_h max_w, max_h }
bounds: { x, y, w, h }
}
id: unique identifier of the element
sizing: the size that the element wants
bounds: the absoulte bounds that the element got assigned
Rendering
=========
Rendering happens when the element is called (immediately for leaf widgets like buttons and at the end
for root widgets like divs). The drawing is done on the bounds assigned to the widget, these bounds
have a one-frame delay on the current layout.
The layout is calculated by each div at the end of their block and at frame end all the sizes and positions
are assigned at frame end by iterating the element tree.
ElemDiv {
align: TOP-LEFT | LEFT | BOTTOM-LEFT | BOTTOM | BOTTOM-RIGHT | RIGHT | TOP-RIGHT | RIGHT | CENTER
size_x/y: { min, max }
scroll_x/y: true | false
layout: ROW | COLUMN
children_size_x/y: { min, max }
}
size:
- min != max -> FIT sizing, fit to the content but respect the min and max size
- min == max == 0 -> GROW sizing, grow to the max amount of space possible
- min == max != 0 -> EXACT sizing
children_size: the size of the combined children sizes
root(size_x: screen width, size_y: screen height, layout: ROW) {
div1(layout: COLUMN, size_x: FIT, size_y GROW, resize_x: true) {
E1()
E2()
E3()
E4()
} <-(end div 1)
div2(size_x: GROW, size_y: GROW) {
...
} <-(end div 2)
div3(layout: COLUMN, size_x: FIT, size_y: GROW) {
E5()
E6()
E7()
} <-(end div 3)
} <-(end root)
(frame end)
+-Root-Div------------------------------------------------+
|+-Div-1----------++-Div-2--------------------++-Div-3---+|
||+--------------+|| ||+-------+||
||| E1 ||| ||| E5 |||
||| ||| ||| |||
||+--------------+|| ||+-------+|| [Root Div]
||+--------------+|| ||+-------+|| |
||| E2 ||| ||| E6 ||| +----------+----+-------+
||| ||| ||| ||| v v v
||+--------------+|| ||+-------+|| [Div 1] [Div 2] [Div 3]
||+------+ || ||+-------+|| | |
||| | || ||| E7 ||| +----+----+----+ |
||| E3 | || ||| ||| v v v v |
||| | || ||+-------+|| [E1] [E2] [E3] [E4] +----+----+
||+------+ || || || v v v
||+------+ || || || [E5] [E6] [E7]
||| | || || ||
||| E4 | || || ||
||| | || || ||
||+------+ || || ||
|| || || ||
|+----------------++--------------------------++---------+|
+---------------------------------------------------------+
the call order is as follows
E1() -> updates the children size of div1
E2() -> " "
E3() -> " "
E4() -> " "
end div1() -> updates the children size of root
end div2() -> updates the children size of root
E5() -> updates the children size of div3
E6() -> " "
E7() -> " "
end root() -> does nothing
at frame end:
* Root: the root has a size constraint of fit so the bounds get assigned the whole window
* Div 1: the width has a size of fit, so it gets set to the children bounds, the height is set
to the root height since it has a height of GROW
- E1 to E4 get laid out
* Div 2: it has a width of GROW which is **along** the layout axis, so it gets added to grow list
the height gets set to the root height
* Div 3: the width is FIT, so it gets set to the content width, the height gets se to the root
height.
- E5 to E7 get laid out
* Div 2: is given a width (if there were other contending grow divs along the layout axis they
would also get sized).
- Now that div 2 has a size all it's children can be given a size
Styling
=======
The element bounds include the whole CSS box model:
+---------------------------------------------+
| MARGIN |
| +-----------------------------------+ |
| |xxxxxxxxxxx BORDER xxxxxxxxxxxx| |
| |x+-------------------------------+x| |
| |x| PADDING |x| |
| |x| +-----------------------+ |x| |
| |x| | | |x| |
| |x| | CONTENT | |x| |
| |x| | | |x| |
| |x| +-----------------------+ |x| |
| |x| |x| |
| |x+-------------------------------+x| |
| |xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| |
| +-----------------------------------+ |
| |
+---------------------------------------------+
Styling happens via a .css file, the sizing strictly refers to the content, so if the the user
requests an exact size of 100px*100px the content box will have those dimensions, but the element
bounds will be larger.

165
LICENSE Normal file
View File

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

1
README.md Normal file
View File

@ -0,0 +1 @@
Welcome to the ugui library.

View File

@ -1,4 +0,0 @@
Divide up the OpenGL renderer into three parts, one part that draws simple shapes,
another that only draws text and one that is responsible for drawing icons and
mapping sprites in general, this way all the texture atlases are separate and
everything is done within 3 draw calls

136
TODO Normal file
View File

@ -0,0 +1,136 @@
# TODOs, semi-random sorting
[x] Implement glyph draw command
[x] Implement div.view and scrollbars
[x] Port font system from C to C3 (rewrite1)
[ ] Update ARCHITECTURE.md
[x] Write a README.md
[x] Use an arena allocator for cache
[ ] Do not redraw if there was no update (no layout and no draw)
[ ] Do command buffer damage tracking based on a context grid (see rxi writeup)
[x] Better handling of the active and focused widgets, try to maintain focus until mouse release (fix scroll bars)
[x] Clip element bounds to parent div, specifically text
[ ] Resizeable divs
[x] Implement a z index and sort command buffer based on that
[ ] Ctx.set_z_index()
[x] Sort command buffer on insertion
[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot
of boiler plate and reuse it
[x] The id combination in gen_id() uses an intger division, which is costly, use another combination
function that is non-linear and doesn't use division
[ ] Animations, somehow
[x] Maybe cache codepoint converted strings
[x] Fix scroll wheel when div is scrolled
[ ] Be consistent with the initialization methods some are foo.new() and some are foo.init()
[ ] Implement image loading (.bmp, .ff, .qoi and .png), in the future even lossy images like .jpg
[x] .qoi
[ ] .ff
[ ] .bmp
[ ] .png
[ ] .jpg
[ ] gif support?
[x] layout_set_max_rows() and layout_set_max_columns()
[x] Maybe SDF sprites??
[x] Stylesheets and stylesheet import
[x] use SDF to draw anti-aliased rounded rectangles https://zed.dev/blog/videogame
[ ] Subdivide modules into ugui::ug for exported functions and ugui::core for
internal use functions (used to create widgets)
[x] The render loop RAPES the gpu, valve pls fix
[x] The way the element structures are implemented wastes a lot of memory since
each struct Elem, struct Cmd, etc. is as big as the largest element. It would
be better to use a different allcation strategy.
[ ] Add a way to handle time events like double clicks
[x] Fix how padding is applied in push_rect. In CSS padding is applied between the border and the
content, the background color is applied starting from the border. Right now push_rect() offsets
the background rect by both border and padding
[x] Investigate why the debug pointer (cyan rectangle) disappears...
## Layout
[x] Flexbox
[x] Center elements to the row/column
[x] Text wrapping / reflow
[x] Implement a better and unified way to place a glyph and get the cursor position, maybe with a struct
[x] Correct whitespace handling in text (\t \r etc)
[x] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay)
instead of the curren multi-frame approach.
[x] Implement column/row sizing (min, max)
[x] Implement a way to size the element as the current row/column size
* +-------------+
* | |
* +-------------+
* +--+
* | |
* +--+
* <------------->
* column size
See the calculator example for why it is useful
[ ] Find a way to concile pixel measurements to the mm ones used in css, for example in min/max sizing
of elements
[x] Center elements to div (center children_bounds to the center of the div bounds and shift the origin accordingly)
[x] Use containing_rect() in position_element() to skip some computing and semplify the function
[x] Rename position_element() to layout_element()
[x] Make functions to mark rows/columns as full, to fix the calculator demo
## Input
[x] Keyboard input
[x] Mouse scroll wheel
[ ] Touch input
[x] Do not set input event to true if the movement was zero (like no mouse movement)
[x] Use input event flags, for example to consume the input event
[x] Fix bug in text box: when spamming keys you can get multiple characters in the text input field
of the context, this causes a bug where only the first char is actually used
## Commands
[x] rect commads should have:
- border width
- border radius
[x] add a command to update an atlas
[x] New window command, useful for popups
[x] Text command returns the text bounds, this way we can avoid the pattern
draw_text(a, pos) -> off = compute_bounds(a) -> draw_text(b, pos+off) -> ...
[ ] Rounded rectangle with different radius for each corner
## Atlas
[ ] Add an interface to create, destroy, update and get atlases based on their ids
[ ] Implement multiple font atlases
[ ] Pixel format conversion
## Fonts
[x] Fix the missing alpha channel
[x] Fix the alignment
## Widgets
[x] Dynamic text box to implement an fps counter
[x] Button with label
[x] Text Input box
[ ] Icon Buttons
[x] Switch
[x] Checkbox
[ ] Selectable text box
## API
[ ] Introduce a Layout structure that specifies the positioning of elements inside
a Div element. This would allow specifying alignment, maximum and minimum sizing
margins between children, etc.
This is different from style which is applied per-element.
[ ] Remove Ids for element that don't need them. Such elements are button, toggles,
and all elements which do not have internal data that has to be cached and/or
queried by the user for later use. This allows for smaller caches and in general
reduces some load, since most of the stuff is recomputed for every frame.
## SDL3 Renderer
[x] smart batching
[x] maybe use instancing since we are always drawing the same geometry. With instancing every
different quad could have its coulour, border and radius with much better performance than
issuing a draw call for every quad (and uploading it)
https://rastertek.com/dx11win10tut48.html
https://www.braynzarsoft.net/viewtutorial/q16390-33-instancing-with-indexed-primitives
[ ] implement min and max fps

View File

@ -1,40 +0,0 @@
#ifndef _UG_DEF_STYLE_H
#define _UG_DEF_STYLE_H
#include "ugui.h"
#define SZ_INT(x) x.size.i
static const ug_style_t default_style = {
.color = {
.bg = RGB_FORMAT(0x131313),
.fg = RGB_FORMAT(0xffffff),
},
.margin = SIZE_PX(3),
.border = {
.color = RGB_FORMAT(0xf50a00),
.size = SIZE_PX(2),
},
.title = {
.color = {
.bg = RGB_FORMAT(0xbbbbbb),
.fg = RGB_FORMAT(0xffff00),
},
.height = SIZE_PX(20),
.font_size = SIZE_PX(14),
},
.btn = {
.color = {
.act = RGB_FORMAT(0x440044),
.bg = RGB_FORMAT(0x006600),
.fg = RGB_FORMAT(0xffff00),
.sel = RGB_FORMAT(0x0a0aff),
.br = RGB_FORMAT(0xff00ff),
},
.font_size = SIZE_PX(10),
.border = SIZE_PX(5),
},
};
#endif

View File

@ -1,3 +0,0 @@
*.ff
*.png
font-to-atlas

View File

@ -1,2 +0,0 @@
font-to-atlas: main.c ff.c ff.h
cc -lm -g main.c ff.c -o font-to-atlas

View File

@ -1,118 +0,0 @@
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "ff.h"
#define MAX(a,b) a>b?a:b
struct ff * ff_new(uint32_t width, uint32_t height)
{
uint64_t size = (uint64_t)width*height*sizeof(struct ff);
struct ff *image = malloc(sizeof(struct ff) + size);
if (!image)
return NULL;
memcpy(image->magic, "farbfeld", 8);
image->width = htonl(width);
image->height = htonl(height);
// create a transparent image
memset(image->pixels, 0, size);
return image;
}
uint64_t ff_bytes(const struct ff *image)
{
if (!image)
return 0;
return sizeof(struct ff)+(uint64_t)ntohl(image->width)*ntohl(image->height)*sizeof(struct ff_px);
}
struct ff * ff_resize(struct ff *image, uint32_t width, uint32_t height)
{
struct ff *new = NULL;
int64_t size = sizeof(struct ff)+(int64_t)width*height*sizeof(uint64_t);
if (image) {
int64_t old_size = ff_bytes(image);
if (old_size == size)
return image;
new = realloc(image, size);
if (!new)
return NULL;
uint32_t old_width = ntohl(new->width);
uint32_t old_height = ntohl(new->height);
if (size-old_size > 0) {
struct ff_px *b = new->pixels;
memset(&b[old_width*old_height], 0, size-old_size);
for (int64_t c = (int64_t)old_height-1; c >= 0; c--) {
memmove(&b[width*c], &b[old_width*c], sizeof(struct ff_px)*old_width);
memset(&b[c*old_width], 0, sizeof(struct ff_px)*(c*width-c*old_width));
}
} else {
}
new->height = htonl(height);
new->width = htonl(width);
} else {
new = ff_new(width, height);
}
return new;
}
int ff_verify (const struct ff *image)
{
if (!image || strncmp("farbfeld", (char*)image->magic, 8))
return 1;
return 0;
}
int ff_free(struct ff *image)
{
if (!ff_verify(image))
free(image);
return 0;
}
// overlays the bitmap containing only 1 8bpp channel to the image starting at (x,y)
// be stands for the data is already big endian
int ff_overlay_8r(struct ff **image, const uint8_t *bitmap, uint32_t x, uint32_t y, uint32_t w, uint32_t h)
{
if (!image || !*image)
return 1;
uint32_t iw = ntohl((*image)->width), ih = ntohl((*image)->height);
*image = ff_resize(*image, MAX(iw, x+w), MAX(ih, y+h));
if (!image)
return -1;
iw = ntohl((*image)->width);
ih = ntohl((*image)->height);
for (uint32_t r = 0; r < h; r++) {
for (uint32_t c = 0; c < w; c++) {
uint8_t col = bitmap[r*w+c];
struct ff_px p = {
.r = 0xffff,
.g = 0xffff,
.b = 0xffff,
.a = htons(257*col)
};
(*image)->pixels[(r+y)*iw + (c+x)] = p;
}
}
return 0;
}

View File

@ -1,25 +0,0 @@
#ifndef _FARBFELD_EZ_H
#define _FARBFELD_EZ_H
#include <stdint.h>
struct __attribute__((packed)) ff_px { uint16_t r, g, b, a; };
struct __attribute__((packed)) ff {
int8_t magic[8];
uint32_t width, height;
struct ff_px pixels[];
};
struct ff * ff_new(uint32_t width, uint32_t height);
int ff_verify (const struct ff *image);
int ff_free(struct ff *image);
uint64_t ff_bytes(const struct ff *image);
struct ff * ff_resize(struct ff *image, uint32_t width, uint32_t height);
int ff_overlay_8r(struct ff **image, const uint8_t *bitmap, uint32_t x, uint32_t y, uint32_t w, uint32_t h);
#endif

View File

@ -1,107 +0,0 @@
#define _POSIX_C_SOURCE 200809l
#define STB_TRUETYPE_IMPLEMENTATION
#define STBTT_STATIC
#include <sys/mman.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <errno.h>
#include <err.h>
#include "stb_truetype.h"
#include "ff.h"
const int font_size = 32;
void map_file(const unsigned char **str, int *size, const char *path)
{
if (!path)
err(EXIT_FAILURE, "NULL filename");
FILE *fp = fopen(path, "r");
if (!fp)
err(EXIT_FAILURE, "Cannot open file %s", path);
*size = lseek(fileno(fp), 0, SEEK_END);
if (*size == (off_t)-1)
err(EXIT_FAILURE, "lseek failed");
*str = mmap(0, *size, PROT_READ, MAP_PRIVATE, fileno(fp), 0);
if (*str == (void*)-1)
err(EXIT_FAILURE, "mmap failed");
if (fclose(fp))
err(EXIT_FAILURE, "Error closing file");
}
int main(int argc, char *argv[])
{
if (argc < 2)
return EXIT_FAILURE;
int len;
const unsigned char *map;
map_file(&map, &len, argv[1]);
stbtt_fontinfo font;
stbtt_InitFont(&font, map, stbtt_GetFontOffsetForIndex(map, 0));
// all this is to get the font bounding box in pixels
float font_scale = 1.0;
font_scale = stbtt_ScaleForPixelHeight(&font, font_size);
int ascent, descent, linegap;
stbtt_GetFontVMetrics(&font, &ascent, &descent, &linegap);
int x0,y0,x1,y1;
int bound_w, bound_h;
stbtt_GetFontBoundingBox(&font, &x0, &y0, &x1, &y1);
printf("font_scale: %f\n", font_scale);
printf("x0:%d y0:%d x1:%d y1:%d\n",x0,y0,x1,y1);
int baseline = font_scale * -y0;
bound_h = (baseline+font_scale*y1) - (baseline+font_scale*y0);
bound_w = (font_scale*x1) - (font_scale*x0);
baseline = bound_h - baseline;
unsigned char *bitmap = malloc(bound_h*bound_w);
if (!bitmap)
err(EXIT_FAILURE, "Cannot allocate bitmap");
printf("bounding h:%d w:%d\n", bound_h, bound_w);
printf("baseline: %d\n", baseline);
struct ff *image = ff_new(0, 0);
// get all ascii
int x = 0, y = 0, maxwidth = 64*bound_w;
for (unsigned int i = 0; i <= 0x7F; i++) {
int x0,y0,x1,y1,w,h,l,a, ox,oy;
int g = stbtt_FindGlyphIndex(&font, i);
stbtt_GetGlyphBitmapBoxSubpixel(&font, g, font_scale, font_scale, 0, 0, &x0, &y0, &x1, &y1);
w = x1 - x0;
h = y1 - y0;
//printf("%d\n", y0);
stbtt_GetGlyphHMetrics(&font, g, &a, &l);
stbtt_MakeGlyphBitmapSubpixel(&font, bitmap, w, h, w, font_scale, font_scale, 0, 0, g);
//printf("'%c' -> l*scale:%.0f, y0:%d\n", i, font_scale*l, bound_h+y0);
ox = font_scale*l;
oy = bound_h+y0;
ff_overlay_8r(&image, bitmap, x+ox, y+oy, w, h);
x += bound_w;
if (x >= maxwidth) y += bound_h;
x %= maxwidth;
}
FILE *fp = fopen("out.ff", "w");
if (fp) {
fwrite(image, 1, ff_bytes(image), fp);
fclose(fp);
}
free(bitmap);
ff_free(image);
munmap((void *)map, len);
return 0;
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

40
input.c
View File

@ -1,40 +0,0 @@
#include "ugui.h"
/*=============================================================================*
* Input Handling *
*=============================================================================*/
#define TEST_CTX(ctx) { if (!ctx) return -1; }
int ug_input_mousemove(ug_ctx_t *ctx, int x, int y)
{
TEST_CTX(ctx)
if (x < 0 || y < 0)
return 0;
ctx->mouse.pos = (ug_vec2_t){.x = x, .y = y};
return 0;
}
int ug_input_mousedown(ug_ctx_t *ctx, unsigned int mask)
{
TEST_CTX(ctx);
ctx->mouse.update |= mask;
return 0;
}
int ug_input_mouseup(ug_ctx_t *ctx, unsigned int mask)
{
TEST_CTX(ctx);
ctx->mouse.update |= mask;
return 0;
}

0
linux-x64/.gitkeep Normal file
View File

11
manifest.json Normal file
View File

@ -0,0 +1,11 @@
{
"provides" : "ugui",
"sources" : [ "src/**" ],
"targets" : {
"linux-x64" : {
"link-args" : [],
"dependencies" : ["schrift"],
"linked-libraries" : []
}
}
}

View File

@ -1,2 +0,0 @@
a.out
gl

View File

@ -1,5 +0,0 @@
CFLAGS = -Wall -Wextra -Wpedantic -std=c11 -g
LDFLAGS = -lSDL2 -lGLEW -lGL
gl: main.c
gcc ${CFLAGS} ${LDFLAGS} main.c -o gl

View File

@ -1,2 +0,0 @@
since I've never worked with opengl I made this folder as a little test environment
for an SDL-OpenGL renderer

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

View File

@ -1,12 +0,0 @@
#version 330
flat in vec4 out_color;
in vec2 texture_coord;
uniform sampler2D texture_sampler;
out vec4 color;
void main()
{
color = texture(texture_sampler, texture_coord) + out_color;
}

View File

@ -1,521 +0,0 @@
#define _POSIX_C_SOURCE 200809l
#include <sys/mman.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <errno.h>
#include <err.h>
#include <SDL2/SDL.h>
#include <GL/glew.h>
#include <SDL2/SDL_opengl.h>
#define GLSL_VERT_SHADER "vertex.glsl"
#define GLSL_FRAG_SHADER "fragment.glsl"
#define PACKED __attribute__((packed))
const int vertindex = 0;
const int colindex = 1;
const int textindex = 2;
struct {
SDL_Window *w;
SDL_GLContext *gl;
GLuint gl_vertbuffer;
GLuint gl_program;
GLuint font_texture;
} ren = {0};
typedef struct PACKED {
union { GLfloat x, u; };
union { GLfloat y, v; };
} vec2;
typedef struct PACKED {
union { GLfloat x, r; };
union { GLfloat y, g; };
union { GLfloat z, b; };
union { GLfloat w, a; };
} vec4;
// a vertex has a position and a color
struct PACKED vertex {
vec2 pos;
vec2 texture;
vec4 color;
};
int w_height(SDL_Window *);
int w_width(SDL_Window *);
// this just descrives a farbfeld image
// https://tools.suckless.org/farbfeld/
struct PACKED _ff {
uint8_t magic[8];
uint32_t w, h;
uint64_t bytes[];
};
const int gw = 7, gh = 9;
unsigned int fw, fh;
struct {
struct vertex *v;
int size, idx;
int prev_idx;
unsigned long int hash, prev_hash;
} vstack = {0};
void grow_stack(int step)
{
vstack.v = realloc(vstack.v, (vstack.size+step)*sizeof(*(vstack.v)));
if(!vstack.v)
err(-1, "Could not allocate stack #S: %s", strerror(errno));
memset(&(vstack.v[vstack.size]), 0, step*sizeof(*(vstack.v)));
vstack.size += step;
}
void push(struct vertex v)
{
if (vstack.idx >= vstack.size)
grow_stack(6);
vstack.v[vstack.idx++] = v;
}
void update_hash()
{
if (!vstack.idx)
return;
unsigned int hash = 0x5400F1B3;
unsigned char *v = (unsigned char *)vstack.v;
int size = vstack.idx;
for (; size; size--) {
hash += v[size-1];
hash += hash << 10;
hash ^= hash >> 6;
}
hash += hash << 3;
hash ^= hash >> 11;
hash += hash << 15;
vstack.hash = hash;
}
void force_changed()
{
vstack.prev_idx = 0;
}
int changed()
{
return vstack.prev_idx != vstack.idx || vstack.prev_hash != vstack.hash;
}
int vstack_push_quad_c(int x, int y, int w, int h, vec4 color)
{
// x4,y4 x3,y3
// +-------------+
// |(x,y) /|
// | / |
// | 2 / |
// | / |
// | / |
// | / 1 |
// |/ |
// +-------------+
// x1,y1 x2,y2
int hw = w_width(ren.w)/2;
int hh = w_height(ren.w)/2;
float x1, x2, x3, x4;
float y1, y2, y3, y4;
x4 = x1 = (float)(x - hw) / hw;
x2 = x3 = (float)(x+w - hw) / hw;
y4 = y3 = (float)(hh - y) / hh;
y1 = y2 = (float)(hh - y-h) / hh;
push((struct vertex){ .pos.x=x1, .pos.y=y1, .color=color });
push((struct vertex){ .pos.x=x2, .pos.y=y2, .color=color });
push((struct vertex){ .pos.x=x3, .pos.y=y3, .color=color });
push((struct vertex){ .pos.x=x1, .pos.y=y1, .color=color });
push((struct vertex){ .pos.x=x3, .pos.y=y3, .color=color });
push((struct vertex){ .pos.x=x4, .pos.y=y4, .color=color });
return 0;
}
int vstack_push_quad_t(int x, int y, int w, int h, int u, int v)
{
// x4,y4 x3,y3
// +-------------+
// |(x,y) /|
// | / |
// | 2 / |
// | / |
// | / |
// | / 1 |
// |/ |
// +-------------+
// x1,y1 x2,y2
int hw = w_width(ren.w)/2;
int hh = w_height(ren.w)/2;
float x1, x2, x3, x4;
float y1, y2, y3, y4;
x1 = x4 = (float)(x - hw) / hw;
x2 = x3 = (float)(x+w - hw) / hw;
y1 = y2 = (float)(hh - y-h) / hh;
y3 = y4 = (float)(hh - y) / hh;
float u1, u2, u3, u4;
float v1, v2, v3, v4;
u1 = u4 = (float)(u) / fw;
u2 = u3 = (float)(u+gw) / fw;
v1 = v2 = (float)(v+gh) / fh;
v3 = v4 = (float)(v) / fh;
push((struct vertex){ .pos.x=x1, .pos.y=y1, .texture.x=u1, .texture.y=v1 });
push((struct vertex){ .pos.x=x2, .pos.y=y2, .texture.x=u2, .texture.y=v2 });
push((struct vertex){ .pos.x=x3, .pos.y=y3, .texture.x=u3, .texture.y=v3 });
push((struct vertex){ .pos.x=x1, .pos.y=y1, .texture.x=u1, .texture.y=v1 });
push((struct vertex){ .pos.x=x3, .pos.y=y3, .texture.x=u3, .texture.y=v3 });
push((struct vertex){ .pos.x=x4, .pos.y=y4, .texture.x=u4, .texture.y=v4 });
return 0;
}
void vstack_clear(void)
{
vstack.prev_hash = vstack.hash;
vstack.prev_idx = vstack.idx;
vstack.idx = 0;
}
// copy the vertex buffer from system to video memory
void ren_initvertbuffer()
{
// generate a buffer id
glGenBuffers(1, &ren.gl_vertbuffer);
// tell opengl that we want to work on that buffer
glBindBuffer(GL_ARRAY_BUFFER, ren.gl_vertbuffer);
// copy the vertex data into the gpu memory, GL_STATIC_DRAW tells opengl
// that the data will be used for drawing (DRAW) and it will only be modified
// every frame (DYNAMIC)
glBufferData(GL_ARRAY_BUFFER, vstack.idx*sizeof(struct vertex), vstack.v, GL_DYNAMIC_DRAW);
// set the format of each vertex, in this case each vertex is made of 4
// coordinates, x y z w, where w is the clip coordinate, stride is
// sizeof(vec4) since every postition vertex there is a vector representing
// color data
// set the position data of the vertex buffer, and bind it as input to the
// vertex shader with an index of 0
glEnableVertexAttribArray(vertindex);
glVertexAttribPointer(vertindex, 2, GL_FLOAT, GL_FALSE, sizeof(struct vertex), 0);
// set the color data of the vertex buffer to index 1
// vertex attribute is the OpenGL name given to a set of vertices which are
// given as input to a vertext shader, in a shader an array of vertices is
// always referred to by index and not by pointer or other manners, indices
// go from 0 to 15
glEnableVertexAttribArray(colindex);
glVertexAttribPointer(colindex, 4, GL_FLOAT, GL_FALSE, sizeof(struct vertex), (void*)(2*sizeof(vec2)));
// texture uv data
glEnableVertexAttribArray(textindex);
glVertexAttribPointer(textindex, 2, GL_FLOAT, GL_FALSE, sizeof(struct vertex), (void*)sizeof(vec2));
// reset the object bind so not to create errors
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
void map_file(const char **str, int *size, const char *fname)
{
FILE *fp = fopen(fname, "r");
*size = lseek(fileno(fp), 0, SEEK_END);
*str = mmap(0, *size, PROT_READ, MAP_PRIVATE, fileno(fp), 0);
fclose(fp);
}
// print shader compilation errors
int debug_shader(GLuint shader)
{
GLint status;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status != GL_FALSE)
return 0;
GLint log_length;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_length);
GLchar *log_str = malloc((log_length + 1)*sizeof(GLchar));
glGetShaderInfoLog(shader, log_length, NULL, log_str);
const char *shader_type_str = NULL;
GLint shader_type;
glGetShaderiv(shader, GL_SHADER_TYPE, &shader_type);
switch(shader_type) {
case GL_VERTEX_SHADER: shader_type_str = "vertex"; break;
case GL_GEOMETRY_SHADER: shader_type_str = "geometry"; break;
case GL_FRAGMENT_SHADER: shader_type_str = "fragment"; break;
}
fprintf(stderr, "Compile failure in %s shader:\n%s\n", shader_type_str, log_str);
free(log_str);
return -1;
}
// print program compilation errors
int debug_program(GLuint prog)
{
GLint status;
glGetProgramiv (prog, GL_LINK_STATUS, &status);
if (status != GL_FALSE)
return 0;
GLint log_length;
glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &log_length);
GLchar *log_str = malloc((log_length + 1)*sizeof(GLchar));
glGetProgramInfoLog(prog, log_length, NULL, log_str);
fprintf(stderr, "Linker failure: %s\n", log_str);
free(log_str);
return -1;
}
void ren_initshaders()
{
GLuint gl_vertshader, gl_fragshader;
// initialize the vertex shader and get the corresponding id
gl_vertshader = glCreateShader(GL_VERTEX_SHADER);
if (!gl_vertshader)
err(-1, "Could not create the vertex shader");
// map the shader file into memory
const char *vshader_str = NULL;
int vshader_size;
map_file(&vshader_str, &vshader_size, GLSL_VERT_SHADER);
// get the shader into opengl
glShaderSource(gl_vertshader, 1, &vshader_str, NULL);
// compile the shader
glCompileShader(gl_vertshader);
if (debug_shader(gl_vertshader))
exit(EXIT_FAILURE);
// do the same for the fragment shader
// FIXME: make this a function
gl_fragshader = glCreateShader(GL_FRAGMENT_SHADER);
if (!gl_fragshader)
err(-1, "Could not create the vertex shader");
// map the shader file into memory
const char *fshader_str = NULL;
int fshader_size;
map_file(&fshader_str, &fshader_size, GLSL_FRAG_SHADER);
// get the shader into opengl
glShaderSource(gl_fragshader, 1, &fshader_str, NULL);
// compile the shader
glCompileShader(gl_fragshader);
if (debug_shader(gl_fragshader))
exit(EXIT_FAILURE);
// create the main program object, it is an amalgamation of all shaders
ren.gl_program = glCreateProgram();
// attach the shaders to the program (set which shaders are present)
glAttachShader(ren.gl_program, gl_vertshader);
glAttachShader(ren.gl_program, gl_fragshader);
// then link the program (basically the linking stage of the program)
glLinkProgram(ren.gl_program);
if (debug_program(ren.gl_program))
exit(EXIT_FAILURE);
// after linking the shaders can be detached and the source freed from
// memory since the program is ready to use
glDetachShader(ren.gl_program, gl_vertshader);
glDetachShader(ren.gl_program, gl_fragshader);
munmap((void *)vshader_str, vshader_size);
munmap((void *)fshader_str, fshader_size);
// now tell opengl to use the program
glUseProgram(ren.gl_program);
}
void ren_drawvertbuffer()
{
if (!changed())
return;
glClear(GL_COLOR_BUFFER_BIT);
glBindBuffer(GL_ARRAY_BUFFER, ren.gl_vertbuffer);
// upload vertex data
if (vstack.idx != vstack.prev_idx)
glBufferData(GL_ARRAY_BUFFER, vstack.idx*sizeof(struct vertex), vstack.v, GL_DYNAMIC_DRAW);
else
glBufferSubData(GL_ARRAY_BUFFER, 0, vstack.idx*sizeof(struct vertex), vstack.v);
// draw vertex data
glDrawArrays(GL_TRIANGLES, 0, vstack.idx);
glBindBuffer(GL_ARRAY_BUFFER, 0);
SDL_GL_SwapWindow(ren.w);
}
int w_height(SDL_Window *win)
{
int h;
SDL_GetWindowSize(win, NULL, &h);
return h;
}
int w_width(SDL_Window *win)
{
int w;
SDL_GetWindowSize(win, &w, NULL);
return w;
}
void import_font(const char *path)
{
const char *map;
int size;
const struct _ff *img;
map_file(&map, &size, path);
img = (const struct _ff *)map;
fw = ntohl(img->w);
fh = ntohl(img->h);
glGenTextures(1, &ren.font_texture);
glBindTexture(GL_TEXTURE_2D, ren.font_texture);
// farbfeld image
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_SHORT, img->bytes);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
munmap((void *)map, size);
}
void push_text(int x, int y, float scale, const char *s)
{
for (; *s; s++) {
int u, v;
int idx = *s - ' ';
u = idx % (fw / gw);
v = (idx / (fw / gw)) % (fh / gh);
vstack_push_quad_t(x, y, gw*scale, gh*scale, u*gw, v*gh);
x += gw*scale;
if (*s == '\n')
y += gh;
}
}
int main (void)
{
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0");
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
ren.w = SDL_CreateWindow("test",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
500, 500, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE );
// create the OpenGL context
ren.gl = SDL_GL_CreateContext(ren.w);
if (ren.gl == NULL)
err(-1, "Failed to create OpenGL context: %s", SDL_GetError());
// select some features
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDisable(GL_CULL_FACE);
glDisable(GL_DEPTH_TEST);
glEnable(GL_SCISSOR_TEST);
glEnable(GL_TEXTURE_2D);
// initialize glew, this library gives us the declarations for most GL
// functions, in the future it would be cool to do it manually
GLenum glew_err = glewInit();
if (glew_err != GLEW_OK)
err(-1, "Failed to initialize GLEW: %s", glewGetErrorString(glew_err));
ren_initshaders();
import_font("./charmap.ff");
vec4 magenta = {.r=1.0, .g=0.0, .b=1.0, .a=1.0};
ren_initvertbuffer();
glClearColor(0.3f, 0.3f, 0.3f, 0.f);
glClear(GL_COLOR_BUFFER_BIT);
// event loop and drawing
SDL_Event ev = {0};
int running = 1;
do {
SDL_WaitEvent(&ev);
switch (ev.type) {
case SDL_QUIT: running = 0; break;
case SDL_WINDOWEVENT:
if(ev.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
glViewport(0, 0, w_width(ren.w), w_height(ren.w));
glScissor(0, 0, w_width(ren.w), w_height(ren.w));
force_changed();
}
break;
default: break;
}
vstack_push_quad_c(0, 0, 100, 100, magenta);
vstack_push_quad_c(200, 0, 10, 10, magenta);
vstack_push_quad_c(10, 150, 100, 100, magenta);
push_text(250, 250, 1.0f, "Ciao Victoria <3");
update_hash();
ren_drawvertbuffer();
vstack_clear();
} while(running);
glUseProgram(0);
glDisableVertexAttribArray(vertindex);
glDisableVertexAttribArray(colindex);
glDeleteTextures(1, &ren.font_texture);
glDeleteBuffers(1, &ren.gl_vertbuffer);
SDL_GL_DeleteContext(ren.gl);
SDL_DestroyWindow(ren.w);
SDL_Quit();
return 0;
}

View File

@ -1,17 +0,0 @@
#version 330
// use the input ("in") vertices from index zero
layout(location = 0) in vec2 position;
layout(location = 1) in vec4 color;
layout(location = 2) in vec2 uv;
flat out vec4 out_color;
out vec2 texture_coord;
void main()
{
// simply copy teh output position
gl_Position = vec4(position.x, position.y, 0.0f, 1.0f);
out_color = color;
texture_coord = uv;
}

2
opengl/.gitignore vendored
View File

@ -1,2 +0,0 @@
a.out
gl

View File

@ -1,5 +0,0 @@
CFLAGS = -Wall -Wextra -Wpedantic -std=c11 -g
LDFLAGS = -lSDL2 -lGLEW -lGL
gl: main.c
gcc ${CFLAGS} ${LDFLAGS} main.c -o gl

View File

@ -1,2 +0,0 @@
since I've never worked with opengl I made this folder as a little test environment
for an SDL-OpenGL renderer

View File

@ -1,10 +0,0 @@
#version 330
smooth in vec4 out_color;
out vec4 color;
void main()
{
// set the color for each vertex to white
color = out_color;
}

View File

@ -1,278 +0,0 @@
#define _POSIX_C_SOURCE 200809l
#include <sys/mman.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <err.h>
#include <SDL2/SDL.h>
#include <GL/glew.h>
#include <SDL2/SDL_opengl.h>
#define GLSL_VERT_SHADER "vertex.glsl"
#define GLSL_FRAG_SHADER "fragment.glsl"
#define PACKED __attribute__((packed))
const int vertindex = 0;
const int colindex = 1;
struct {
SDL_Window *w;
SDL_GLContext *gl;
GLuint gl_vertbuffer;
GLuint gl_program;
} ren = {0};
typedef struct PACKED {
union { GLfloat x, r; };
union { GLfloat y, g; };
union { GLfloat z, b; };
union { GLfloat w, a; };
} vec4;
// a vertex has a position and a color
struct PACKED vertex { vec4 pos, col; };
#define VNUM(s) (sizeof(s)/sizeof(s[0]))
struct vertex vertbuffer[] = {
{ .pos={.x= 0.75f, .y= 0.75f, .z=0.0f,.w=1.0f}, .col={ .r=1.0f, .g=0.0f, .b=0.0f, .a=1.0f} },
{ .pos={.x=-0.75f, .y=-0.75f, .z=0.0f,.w=1.0f}, .col={ .r=1.0f, .g=0.0f, .b=0.0f, .a=1.0f} },
{ .pos={.x= 0.75f, .y=-0.75f, .z=0.0f,.w=1.0f}, .col={ .r=1.0f, .g=0.0f, .b=0.0f, .a=1.0f} },
{ .pos={.x= 0.75f, .y= 0.75f, .z=0.0f,.w=1.0f}, .col={ .r=1.0f, .g=0.0f, .b=0.0f, .a=1.0f} },
{ .pos={.x=-0.75f, .y=-0.75f, .z=0.0f,.w=1.0f}, .col={ .r=1.0f, .g=0.0f, .b=0.0f, .a=1.0f} },
{ .pos={.x=-0.75f, .y= 0.75f, .z=0.0f,.w=1.0f}, .col={ .r=1.0f, .g=0.0f, .b=0.0f, .a=1.0f} },
};
// copy the vertex buffer from system to video memory
void ren_initvertbuffer()
{
// generate a buffer id
glGenBuffers(1, &ren.gl_vertbuffer);
// tell opengl that we want to work on that buffer
glBindBuffer(GL_ARRAY_BUFFER, ren.gl_vertbuffer);
// copy the vertex data into the gpu memory, GL_STATIC_DRAW tells opengl
// that the data will be used for drawing (DRAW) and it will only be modified
// once (STATIC)
glBufferData(GL_ARRAY_BUFFER, sizeof(vertbuffer), &vertbuffer, GL_STATIC_DRAW);
// set the format of each vertex, in this case each vertex is made of 4
// coordinates, x y z w, where w is the clip coordinate, stride is
// sizeof(vec4) since every postition vertex there is a vector representing
// color data
// set the position data of the vertex buffer, and bind it as input to the
// vertex shader with an index of 0
glEnableVertexAttribArray(vertindex);
glVertexAttribPointer(vertindex, 4, GL_FLOAT, GL_FALSE, sizeof(struct vertex), 0);
// set the color data of the vertex buffer to index 1
// vertex attribute is the OpenGL name given to a set of vertices which are
// given as input to a vertext shader, in a shader an array of vertices is
// always referred to by index and not by pointer or other manners, indices
// go from 0 to 15
glEnableVertexAttribArray(colindex);
glVertexAttribPointer(colindex, 4, GL_FLOAT, GL_FALSE, sizeof(struct vertex), (void*)sizeof(vec4));
// reset the object bind so not to create errors
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
void map_file(const char **str, int *size, const char *fname)
{
FILE *fp = fopen(fname, "r");
*size = lseek(fileno(fp), 0, SEEK_END);
*str = mmap(0, *size, PROT_READ, MAP_PRIVATE, fileno(fp), 0);
fclose(fp);
}
// print shader compilation errors
int debug_shader(GLuint shader)
{
GLint status;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status != GL_FALSE)
return 0;
GLint log_length;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_length);
GLchar *log_str = malloc((log_length + 1)*sizeof(GLchar));
glGetShaderInfoLog(shader, log_length, NULL, log_str);
const char *shader_type_str = NULL;
GLint shader_type;
glGetShaderiv(shader, GL_SHADER_TYPE, &shader_type);
switch(shader_type) {
case GL_VERTEX_SHADER: shader_type_str = "vertex"; break;
case GL_GEOMETRY_SHADER: shader_type_str = "geometry"; break;
case GL_FRAGMENT_SHADER: shader_type_str = "fragment"; break;
}
fprintf(stderr, "Compile failure in %s shader:\n%s\n", shader_type_str, log_str);
free(log_str);
return -1;
}
// print program compilation errors
int debug_program(GLuint prog)
{
GLint status;
glGetProgramiv (prog, GL_LINK_STATUS, &status);
if (status != GL_FALSE)
return 0;
GLint log_length;
glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &log_length);
GLchar *log_str = malloc((log_length + 1)*sizeof(GLchar));
glGetProgramInfoLog(prog, log_length, NULL, log_str);
fprintf(stderr, "Linker failure: %s\n", log_str);
free(log_str);
return -1;
}
void ren_initshaders()
{
GLuint gl_vertshader, gl_fragshader;
// initialize the vertex shader and get the corresponding id
gl_vertshader = glCreateShader(GL_VERTEX_SHADER);
if (!gl_vertshader)
err(-1, "Could not create the vertex shader");
// map the shader file into memory
const char *vshader_str = NULL;
int vshader_size;
map_file(&vshader_str, &vshader_size, GLSL_VERT_SHADER);
// get the shader into opengl
glShaderSource(gl_vertshader, 1, &vshader_str, NULL);
// compile the shader
glCompileShader(gl_vertshader);
if (debug_shader(gl_vertshader))
exit(EXIT_FAILURE);
// do the same for the fragment shader
// FIXME: make this a function
gl_fragshader = glCreateShader(GL_FRAGMENT_SHADER);
if (!gl_fragshader)
err(-1, "Could not create the vertex shader");
// map the shader file into memory
const char *fshader_str = NULL;
int fshader_size;
map_file(&fshader_str, &fshader_size, GLSL_FRAG_SHADER);
// get the shader into opengl
glShaderSource(gl_fragshader, 1, &fshader_str, NULL);
// compile the shader
glCompileShader(gl_fragshader);
if (debug_shader(gl_fragshader))
exit(EXIT_FAILURE);
// create the main program object, it is an amalgamation of all shaders
ren.gl_program = glCreateProgram();
// attach the shaders to the program (set which shaders are present)
glAttachShader(ren.gl_program, gl_vertshader);
glAttachShader(ren.gl_program, gl_fragshader);
// then link the program (basically the linking stage of the program)
glLinkProgram(ren.gl_program);
if (debug_program(ren.gl_program))
exit(EXIT_FAILURE);
// after linking the shaders can be detached and the source freed from
// memory since the program is ready to use
glDetachShader(ren.gl_program, gl_vertshader);
glDetachShader(ren.gl_program, gl_fragshader);
munmap((void *)vshader_str, vshader_size);
munmap((void *)fshader_str, fshader_size);
// now tell opengl to use the program
glUseProgram(ren.gl_program);
}
void ren_drawvertbuffer()
{
glBindBuffer(GL_ARRAY_BUFFER, ren.gl_vertbuffer);
glDrawArrays(GL_TRIANGLES, 0, VNUM(vertbuffer));
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
int w_height(SDL_Window *win)
{
int h;
SDL_GetWindowSize(win, NULL, &h);
return h;
}
int w_width(SDL_Window *win)
{
int w;
SDL_GetWindowSize(win, &w, NULL);
return w;
}
int main (void)
{
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0");
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
ren.w = SDL_CreateWindow("test",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
500, 500, SDL_WINDOW_OPENGL );
// create the OpenGL context
ren.gl = SDL_GL_CreateContext(ren.w);
if (ren.gl == NULL)
err(-1, "Failed to create OpenGL context: %s", SDL_GetError());
// initialize glew, this library gives us the declarations for most GL
// functions, in the future it would be cool to do it manually
GLenum glew_err = glewInit();
if (glew_err != GLEW_OK)
err(-1, "Failed to initialize GLEW: %s", glewGetErrorString(glew_err));
ren_initvertbuffer();
ren_initshaders();
// event loop and drawing
SDL_Event ev = {0};
int running = 1;
do {
SDL_WaitEvent(&ev);
switch (ev.type) {
case SDL_QUIT: running = 0; break;
default: break;
}
glViewport(0, 0, w_width(ren.w), w_height(ren.w));
glClearColor(0.f, 0.f, 0.f, 0.f);
glClear(GL_COLOR_BUFFER_BIT);
ren_drawvertbuffer();
SDL_GL_SwapWindow(ren.w);
} while(running);
glDisableVertexAttribArray(vertindex);
glDisableVertexAttribArray(colindex);
SDL_GL_DeleteContext(ren.gl);
SDL_DestroyWindow(ren.w);
SDL_Quit();
return 0;
}

View File

@ -1,14 +0,0 @@
#version 330
// use the input ("in") vertices from index zero
layout(location = 0) in vec4 position;
layout(location = 1) in vec4 color;
smooth out vec4 out_color;
void main()
{
// simply copy teh output position
gl_Position = position;
out_color = color;
}

View File

@ -1,126 +0,0 @@
#ifndef _STACK_GENERIC_H
#define _STACK_GENERIC_H
#define STACK_STEP 8
#define STACK_SALT 0xbabb0cac
// FIXME: find a way to not re-hash the whole stack when removing one item
// incremental hash for every grow
#define STACK_HASH(p, s, h) \
{ \
unsigned char *v = (unsigned char *)(p); \
for (int x = (s); x; x--) { \
(h) += v[x-1]; \
(h) += (h) << 10; \
(h) ^= (h) >> 6; \
} \
(h) += (h) << 3; \
(h) ^= (h) >> 11; \
(h) += (h) << 15; \
}
// declare just the prototypes
#define STACK_PROTO(stackname, type) \
struct stackname { \
type *items; \
int size, idx, old_idx; \
unsigned int hash, old_hash; \
}; \
struct stackname stackname##_init(void); \
int stackname##_grow(struct stackname *stack, int step); \
int stackname##_push(struct stackname *stack, type *e); \
type stackname##_pop(struct stackname *stack); \
int stackname##_clear(struct stackname *stack); \
int stackname##_changed(struct stackname *stack); \
int stackname##_size_changed(struct stackname *stack); \
int stackname##_free(struct stackname *stack);
#define STACK_DEFINE(stackname, type) \
struct stackname; \
\
\
struct stackname stackname##_init(void) \
{ \
return (struct stackname){0, .hash = STACK_SALT}; \
} \
\
\
int stackname##_grow(struct stackname *stack, int step) \
{ \
if (!stack) \
return -1; \
stack->items = realloc(stack->items, (stack->size+step)*sizeof(type)); \
if(!stack->items) \
return -1; \
memset(&(stack->items[stack->size]), 0, step*sizeof(*(stack->items))); \
stack->size += step; \
return 0; \
} \
\
\
int stackname##_push(struct stackname *stack, type *e) \
{ \
if (!stack || !e) \
return -1; \
if (stack->idx >= stack->size) \
if (stackname##_grow(stack, STACK_STEP)) \
return -1; \
stack->items[stack->idx++] = *e; \
STACK_HASH(e, sizeof(type), stack->hash); \
return 0; \
} \
\
\
type stackname##_pop(struct stackname *stack) \
{ \
if (!stack || stack->idx == 0 || stack->size == 0) \
return (type){0}; \
stack->hash = STACK_SALT; \
STACK_HASH(stack->items, sizeof(type)*(stack->idx-1), stack->hash); \
return stack->items[stack->idx--]; \
} \
\
\
int stackname##_clear(struct stackname *stack) \
{ \
if (!stack) \
return -1; \
stack->old_idx = stack->idx; \
stack->old_hash = stack->hash; \
stack->hash = STACK_SALT; \
stack->idx = 0; \
return 0; \
} \
\
\
int stackname##_changed(struct stackname *stack) \
{ \
if (!stack) \
return -1; \
return stack->hash != stack->old_hash; \
} \
\
\
int stackname##_size_changed(struct stackname *stack) \
{ \
if (!stack) \
return -1; \
return stack->size != stack->old_idx; \
} \
\
\
int stackname##_free(struct stackname *stack) \
{ \
if (stack) { \
stackname##_clear(stack); \
if (stack->items) \
free(stack->items); \
} \
return 0; \
} \
#endif

0
scripts/.gitkeep Normal file
View File

131
src/atlas.c3 Normal file
View File

@ -0,0 +1,131 @@
module ugui;
import std::io;
faultdef CANNOT_PLACE, INVALID_TYPE;
enum AtlasType {
ATLAS_GRAYSCALE,
ATLAS_R8G8B8A8,
}
// black and white atlas
struct Atlas {
AtlasType type;
Id id;
ushort width, height;
char[] buffer;
Point row;
ushort row_h;
}
// bytes per pixel
macro usz AtlasType.bpp(type)
{
switch (type) {
case ATLAS_GRAYSCALE: return 1;
case ATLAS_R8G8B8A8: return 4;
}
}
macro typeid AtlasType.underlying(type)
{
switch (type) {
case ATLAS_GRAYSCALE: return char;
case ATLAS_R8G8B8A8: return uint;
}
}
/*
// FIXME: in and out types are not always known at compile time
macro @pixel_convert(p, AtlasType $in, AtlasType $out)
{
$if $in == $out:
return p;
$else
$switch
$case $in == ATLAS_R8G8B8A8 && $out == ATLAS_GRAYSCALE:
var r = ((p >> 0) & 0xff);
var g = ((p >> 8) & 0xff);
var b = ((p >> 16) & 0xff);
var a = ((p >> 24) & 0xff);
if (a == 0) return (char)0;
return (ATLAS_GRAYSCALE.underlying())(((float)r+g+b) / 3.0f);
$case $in == ATLAS_GRAYSCALE && $out == ATLAS_R8G8B8A8:
var x = (char)(p/3.0);
return (ATLAS_R8G8B8A8.underlying())(x|(x<<8)|(x<<16)|(255<<24));
$default: $error "Unimplemented pixel format conversion";
$endswitch
$endif
}
*/
fn void? Atlas.new(&atlas, Id id, AtlasType type, ushort width, ushort height)
{
atlas.id = id;
atlas.type = type;
atlas.width = width;
atlas.height = height;
atlas.buffer = mem::new_array(char, (usz)atlas.width*atlas.height*type.bpp());
}
fn void Atlas.free(&atlas)
{
free(atlas.buffer);
}
/*
* pixels -> +--------------+-----+
* | | | h
* | | | e
* | | | i
* | | | g
* | | | h
* | | | t
* +--------------+-----+
* |<--- width -->|
* |<----- stride ----->|
* bytes per pixels are inferred and have to be the same
* as the atlas type
*/
// place a rect inside the atlas
// uses a row first algorithm
// TODO: use a skyline algorithm https://jvernay.fr/en/blog/skyline-2d-packer/implementation/
fn Point? Atlas.place(&atlas, char[] pixels, ushort w, ushort h, ushort stride)
{
Point p;
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
p = atlas.row;
} else {
atlas.row.x = 0;
atlas.row.y = atlas.row.y + atlas.row_h;
atlas.row_h = 0;
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
p = atlas.row;
} else {
return CANNOT_PLACE?;
}
}
usz bpp = atlas.type.bpp();
for (usz y = 0; y < h; y++) {
for (usz x = 0; x < w; x++) {
char[] buf = atlas.buffer[(usz)(p.y+y)*atlas.width*bpp + (p.x+x)*bpp ..];
char[] pix = pixels[(usz)y*stride*bpp + x*bpp ..];
buf[0..bpp-1] = pix[0..bpp-1];
}
}
atlas.row.x += w;
if (h > atlas.row_h) {
atlas.row_h = h;
}
return p;
}

149
src/cache.c3 Normal file
View File

@ -0,0 +1,149 @@
module cache{Key, Value, SIZE};
/* LRU Cache
* The cache uses a pool (array) to store all the elements, each element has
* a key (id) and a value. A HashMap correlates the ids to an index in the pool.
* To keep track of which items were recently used two bit arrays are kept, one
* stores the "used" flag for each index and anothe the "present" flag.
* Every NCYCLES operations the present and used arrays are updated to free up
* the elements that were not recently used.
*/
import std::core::mem;
import std::core::mem::allocator;
import std::collections::bitset;
import std::collections::map;
alias BitArr = bitset::BitSet{SIZE};
alias IdTable = map::HashMap{Key, usz};
alias IdTableEntry = map::Entry{Key, usz};
const usz CACHE_NCYCLES = (usz)(SIZE * 2.0/3.0);
struct Cache {
Allocator allocator;
BitArr present, used;
IdTable table;
Value[] pool;
usz cycle_count;
}
// Every CACHE_CYCLES operations mark as not-present the unused elements
macro Cache.cycle(&cache) @private {
cache.cycle_count++;
if (cache.cycle_count > CACHE_NCYCLES) {
for (usz i = 0; i < cache.present.data.len; i++) {
cache.present.data[i] &= cache.used.data[i];
cache.used.data[i] = 0;
}
cache.cycle_count = 0;
}
}
fn void? Cache.init(&cache, Allocator allocator)
{
cache.allocator = allocator;
cache.table.init(allocator, capacity: SIZE);
// FIXME: this shit is SLOW
foreach (idx, bit : cache.used) { cache.used[idx] = false; }
foreach (idx, bit : cache.present) { cache.present[idx] = false; }
cache.pool = allocator::new_array(allocator, Value, SIZE);
}
fn void Cache.free(&cache)
{
(void)cache.table.free();
(void)allocator::free(cache.allocator, cache.pool);
}
fn Value*? Cache.search(&cache, Key id)
{
// get_entry() faults on miss
IdTableEntry* entry = cache.table.get_entry(id)!;
/* MISS, wrong key */
if (entry.key != id) {
cache.table.remove(id)!;
return NOT_FOUND?;
}
/* MISS, the data is not valid (not present) */
if (!cache.present[entry.value]) {
// if the data is not present but it is still in the table, remove it
cache.table.remove(id)!;
return NOT_FOUND?;
}
/* HIT, set as recently used */
//io::printfn("HIT: %d [%d]", entry.value, entry.key);
cache.used[entry.value] = true;
return &(cache.pool[entry.value]);
}
fn void Cache.remove(&cache, Key id)
{
IdTableEntry*? entry = cache.table.get_entry(id);
if (catch entry) {
return;
}
// found, remove it
cache.present[entry.value] = false;
(void)cache.table.remove(id);
}
/* Look for a free spot in the present bitmap and return its index */
/* If there is no free space left then just return the first position */
fn usz Cache.get_free_spot(&cache) @private
{
// TODO: in the upgrade to c3 1.7.5 use @bitsof()
const BITS = $typeof(cache.present.data[0]).sizeof*8;
foreach (idx, d: cache.present.data) {
if (d != $typeof(d).max) {
usz spot = idx*BITS + BITS-d.clz();
if (cache.used[spot]) unreachable("free spot is not actually free: %d", spot);
return spot;
}
}
return 0;
}
fn Value*? Cache.insert_at(&cache, Value* g, Key id, usz index) @private
{
// TODO: verify index, g and id
Value* spot;
/* Set used and present */
cache.present.set(index);
cache.used.set(index);
cache.cycle();
spot = &(cache.pool[index]);
*spot = *g;
cache.table.set(id, index);
return spot;
}
// Insert an element in the cache, returns the index
fn Value*? Cache.insert_new(&cache, Value* g, Key id)
{
usz index = cache.get_free_spot();
return cache.insert_at(g, id, index);
}
fn Value*? Cache.get_or_insert(&cache, Value* g, Key id, bool *is_new = null)
{
Value*? c = cache.search(id);
if (catch e = c) {
if (e != NOT_FOUND) {
return e?;
} else {
// if the element is new (inserted) set the is_new flag
if (is_new) *is_new = true;
return cache.insert_new(g, id);
}
} else {
if (is_new) *is_new = false;
return c;
}
}

217
src/cmd.c3 Normal file
View File

@ -0,0 +1,217 @@
module ugui;
import std::io;
import std::collections::list;
// command type
enum CmdType {
CMD_RECT,
CMD_UPDATE_ATLAS,
CMD_SPRITE,
CMD_SCISSOR,
CMD_REQ_SKIP_FRAME,
}
// command to draw a rect
struct CmdRect {
Rect rect;
ushort radius;
Color color;
}
struct CmdUpdateAtlas {
Id id;
char* raw_buffer;
short width, height, bpp;
}
struct CmdSprite {
Id texture_id;
SpriteType type;
Rect rect;
Rect texture_rect;
Color hue;
}
// if rect is zero Rect{0} then reset the scissor
struct CmdScissor {
Rect rect;
}
// command structure
struct Cmd (Printable) {
CmdType type;
int z_index;
union {
CmdRect rect;
CmdUpdateAtlas update_atlas;
CmdSprite sprite;
CmdScissor scissor;
}
}
// command queue
alias CmdQueue = list::List{Cmd};
fn int Cmd.compare_to(Cmd a, Cmd b)
{
if (a.z_index == b.z_index) return 0;
return a.z_index > b.z_index ? 1 : -1;
}
// FIXME: This sorting method does not fully respect layer ordering for popups.
// Popups that start on the same layer but are enqueued at different times
// may still be rendered on top of each other, even if the first popup
// has elements in higher layers than the second. The current approach
// does not truly sort by layer; it only moves elements from higher layers
// to the end of the queue as they are encountered.
fn void? CmdQueue.sort(&queue)
{
CmdQueue stack;
stack.init(allocator::tmem);
for (isz i = queue.len()-1; i > 0; i--) {
Cmd cur = (*queue)[i];
Cmd next = (*queue)[i - 1];
// cur < next
if (cur.compare_to(next) < 0) {
stack.push(next);
queue.remove_at(i-1);
}
}
usz l = stack.len();
for (usz i; i < l; i++) {
queue.push(stack.pop())!;
}
}
// implement the Printable interface
fn usz? Cmd.to_format(Cmd* cmd, Formatter *f) @dynamic
{
usz ret;
ret += f.printf("Cmd{ type: %s, z_index: %d, ", cmd.type, cmd.z_index)!;
switch (cmd.type) {
case CMD_RECT:
ret += f.print("CmdRect")!;
ret += io::struct_to_format(cmd.rect, f, false)!;
case CMD_SCISSOR:
ret += f.print("CmdScissor")!;
ret += io::struct_to_format(cmd.scissor, f, false)!;
case CMD_SPRITE:
ret += f.print("CmdSprite")!;
ret += io::struct_to_format(cmd.sprite, f, false)!;
case CMD_UPDATE_ATLAS:
ret += f.print("CmdUpdateAtlas")!;
ret += io::struct_to_format(cmd.update_atlas, f, false)!;
case CMD_REQ_SKIP_FRAME:
ret += f.print("Skip Frame Request")!;
}
ret += f.print("}")!;
return ret;
}
macro bool cull_rect(Rect rect, Rect clip = {0,0,short.max,short.max})
{
bool no_area = rect.w <= 0 || rect.h <= 0;
return no_area || !rect.collides(clip);
}
// FIXME: this whole thing could be done at compile time, maybe
macro Ctx.push_cmd(&ctx, Cmd cmd, int z_index)
{
cmd.z_index = z_index;
Rect rect;
switch (cmd.type) {
case CMD_RECT: rect = cmd.rect.rect;
case CMD_SPRITE: rect = cmd.sprite.rect;
default: return ctx.cmd_queue.push(cmd);
}
if (cull_rect(rect, ctx.div_scissor)) {
// println("NOPE: ", cmd.rect.rect, cmd.z_index);
// unreachable();
return;
}
return ctx.cmd_queue.push(cmd);
}
fn void? Ctx.push_scissor(&ctx, Rect rect, int z_index)
{
Cmd sc = {
.type = CMD_SCISSOR,
.scissor.rect = rect.intersection(ctx.div_scissor),
};
ctx.push_cmd(sc, z_index);
}
fn void? Ctx.reset_scissor(&ctx, int z_index) => ctx.push_cmd({.type=CMD_SCISSOR,.scissor.rect=ctx.div_scissor}, z_index);
fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style)
{
Rect border = style.border;
ushort radius = style.radius;
Color bg = style.bg;
Color border_color = style.secondary;
// FIXME: this implies that the border has to be uniform
if (!border.is_null()) {
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = rect,
.rect.color = border_color,
.rect.radius = radius + border.x,
};
ctx.push_cmd(cmd, z_index);
}
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = {
.x = rect.x + border.x,
.y = rect.y + border.y,
.w = rect.w - (border.x+border.w),
.h = rect.h - (border.y+border.h),
},
.rect.color = bg,
.rect.radius = radius,
};
ctx.push_cmd(cmd, z_index);
}
// TODO: accept a Sprite* instead of all this shit
fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_index, Color hue = 0xffffffffu.to_rgba(), SpriteType type = SPRITE_NORMAL)
{
Cmd cmd = {
.type = CMD_SPRITE,
.sprite.type = type,
.sprite.rect = bounds,
.sprite.texture_rect = texture,
.sprite.texture_id = texture_id,
.sprite.hue = hue,
};
ctx.push_cmd(cmd, z_index);
}
fn void? Ctx.push_update_atlas(&ctx, Atlas* atlas)
{
Cmd up = {
.type = CMD_UPDATE_ATLAS,
.update_atlas = {
.id = atlas.id,
.raw_buffer = atlas.buffer,
.width = atlas.width,
.height = atlas.height,
.bpp = (ushort)atlas.type.bpp(),
},
};
// update the atlases before everything else
ctx.push_cmd(up, -1);
}
macro Ctx.dbg_rect(&ctx, Rect r, uint c = 0xff000042u) => ctx.push_rect(r, int.max, &&(Style){.bg=c.to_rgba()})!!;

396
src/core.c3 Normal file
View File

@ -0,0 +1,396 @@
module ugui;
import mtree;
import cache;
import std::io;
import std::core::string;
import std::core::mem::allocator;
import std::collections::pair;
import std::sort;
macro println(...)
{
$for var $i = 0; $i < $vacount; $i++:
io::print($vaexpr[$i]);
$endfor
io::printn();
}
// element ids are just long ints
alias Id = uint;
enum ElemType {
ETYPE_NONE,
ETYPE_DIV,
ETYPE_BUTTON,
ETYPE_SLIDER,
ETYPE_TEXT,
ETYPE_SPRITE,
}
bitstruct ElemFlags : uint {
bool updated : 0;
bool is_new : 1; // element is new in the cache
bool shown : 2; // element has been shown (drawn) this frame
}
bitstruct ElemEvents : uint {
bool key_press : 0;
bool key_release : 1;
bool key_repeat : 2;
bool mouse_hover : 3;
bool mouse_press : 4;
bool mouse_release : 5;
bool mouse_hold : 6;
bool update : 7;
bool text_input : 8;
bool has_focus : 9;
}
// element structure
struct Elem {
Id id;
int tree_idx;
ElemFlags flags;
ElemEvents events;
Rect bounds;
Rect children_bounds;
ElemType type;
Layout layout;
int z_index;
union {
ElemDiv div;
ElemButton button;
ElemSlider slider;
ElemText text;
ElemSprite sprite;
}
}
const uint MAX_ELEMENTS = 256;
const uint MAX_COMMANDS = 2048;
const uint STACK_STEP = 10;
const uint ROOT_ID = 1;
const uint TEXT_MAX = 64;
// Tuple of Element pointers, used when it is useful to get both parent and child
alias PElemTuple = pair::Pair{Elem*, Elem*};
// relationships between elements are stored in a tree, it stores just the ids
alias IdTree = mtree::MTree{Id};
// elements themselves are kept in a cache
alias ElemCache = cache::Cache{Id, Elem, MAX_ELEMENTS};
faultdef INVALID_SIZE, EVENT_UNSUPPORTED, WRONG_ELEMENT_TYPE, WRONG_ID;
struct InputData {
InputEvents events;
struct mouse {
Point pos, delta;
// mouse_down: bitmap of mouse buttons that are held
// mouse_updated: bitmap of mouse buttons that have been updated
// mouse_released = mouse_updated & ~mouse_down
// mouse_pressed = mouse_updated & mouse_down
MouseButtons down;
MouseButtons updated;
// scroll wheel
Point scroll;
}
struct keyboard {
char[TEXT_MAX] text;
usz text_len;
ModKeys modkeys;
}
}
struct Ctx {
IdTree tree;
ElemCache cache;
CmdQueue cmd_queue;
StyleMap styles;
// total size in pixels of the context
ushort width, height;
Font font;
SpriteAtlas sprite_atlas;
bool skip_frame;
bool has_focus;
InputData input, current_input;
Id hover_id;
Id focus_id;
Rect div_scissor; // the current div bounds used for scissor test
int active_div; // tree node indicating the current active div
}
// return a pointer to the parent of the current active div
fn Elem*? Ctx.get_parent(&ctx)
{
Id parent_id = ctx.tree[ctx.active_div]!;
Elem* parent = ctx.cache.search(parent_id)!;
return parent;
}
macro @bits(#a) => $typeof(#a).sizeof*8;
macro Id.rotate_left(id, uint $n) => (id << $n) | (id >> (@bits(id) - $n));
const uint GOLDEN_RATIO = 0x9E3779B9;
// generate an id combining the hashes of the parent id and the label
// with the Cantor pairing function
fn Id? Ctx.gen_id(&ctx, Id id2)
{
// FIXME: this is SHIT
Id id1 = ctx.tree.get(ctx.active_div)!;
// Mix the two IDs non-linearly
Id mixed = id1 ^ id2.rotate_left(13);
mixed ^= id1.rotate_left(7);
mixed += GOLDEN_RATIO;
return mixed;
}
// compute the id from arguments and the line of the call
macro Id @compute_id(...)
{
Id id = (Id)$$LINE.hash() ^ (Id)@str_hash($$FILE);
$for var $i = 0; $i < $vacount; $i++:
id ^= (Id)$vaexpr[$i].hash();
$endfor
return id;
}
// get or push an element from the cache, return a pointer to it
// resets all flags except is_new which is set accordingly
fn PElemTuple? Ctx.get_elem(&ctx, Id id, ElemType type)
{
bool is_new;
Elem* parent;
Elem* elem;
parent = ctx.get_parent() ?? &&(Elem){};
elem = ctx.cache.get_or_insert(&&(Elem){}, id, &is_new)!;
elem.flags = (ElemFlags)0;
elem.flags.is_new = is_new;
elem.flags.shown = true;
elem.id = id;
elem.layout = {};
if (is_new == false && elem.type != type) {
return WRONG_ELEMENT_TYPE?;
} else {
elem.type = type;
}
elem.z_index = parent.z_index;
elem.tree_idx = ctx.tree.add(ctx.active_div, id)!;
return {elem, parent};
}
// find an element, does not allocate a new one in cache
// THIS HAS TO BE A MACRO SINCE IT RETURNS A POINTER TO A TEMPORARY VALUE
macro Elem* Ctx.find_elem(&ctx, Id id)
{
Elem*? elem;
elem = ctx.cache.search(id);
if (catch elem) {
return &&(Elem){};
}
return elem;
}
fn Elem*? Ctx.get_active_div(&ctx)
{
Id id = ctx.tree.get(ctx.active_div)!;
return ctx.cache.search(id);
}
fn void? Ctx.init(&ctx, Allocator allocator)
{
ctx.tree.init(MAX_ELEMENTS, allocator);
defer catch { (void)ctx.tree.free(); }
ctx.cache.init(allocator)!;
defer catch { (void)ctx.cache.free(); }
ctx.cmd_queue.init(allocator::mem, MAX_COMMANDS);
defer catch { (void)ctx.cmd_queue.free(); }
ctx.styles.init(allocator);
ctx.styles.register_style(&DEFAULT_STYLE, @str_hash("default"));
defer catch { ctx.styles.free(); }
ctx.active_div = 0;
}
fn void Ctx.free(&ctx)
{
(void)ctx.tree.free();
(void)ctx.cache.free();
(void)ctx.cmd_queue.free();
(void)ctx.font.free();
(void)ctx.sprite_atlas.free();
(void)ctx.styles.free();
}
fn void? Ctx.frame_begin(&ctx)
{
// 1. Reset the active div
// 2. Get the root element from the cache and update it
ctx.active_div = 0;
Elem* elem = ctx.get_elem(ROOT_ID, ETYPE_DIV)!.first;
ctx.active_div = elem.tree_idx;
// The root should have the updated flag only if the size of the window
// was changed between frasmes, this propagates an element size recalculation
// down the element tree
elem.flags.updated = ctx.input.events.resize;
// if the window has focus then the root element also has focus, no other
// computation needed, child elements need to check the mouse positon and
// other stuff
//elem.flags.has_focus = ctx.has_focus;
elem.bounds = {0, 0, ctx.width, ctx.height};
elem.z_index = 0;
elem.div.scroll_x.enabled = false;
elem.div.scroll_y.enabled = false;
elem.layout.dir = ROW;
elem.layout.anchor = TOP_LEFT;
elem.layout.w = @exact(ctx.width);
elem.layout.h = @exact(ctx.height);
ctx.div_scissor = elem.bounds;
ctx.skip_frame = false;
}
fn void? Ctx.frame_end(&ctx)
{
Elem* root = ctx.get_active_div()!;
if (root.id != ROOT_ID) {
return WRONG_ID?;
}
// 2. clear input fields
ctx.input = ctx.current_input;
ctx.current_input.events = {};
ctx.current_input.mouse.scroll = {};
ctx.current_input.mouse.updated = BTN_NONE;
ctx.current_input.keyboard.text_len = 0;
// DO THE LAYOUT
ctx.layout_element_tree()!;
foreach (idx, id : ctx.tree.elem_vec) {
if (!ctx.tree.is_used((int)idx)) continue;
Elem* c = ctx.find_elem(id);
// reset events
c.events = {};
// reset shown flag
// TODO: use shown_last_frame to avoid this loop entirely
c.flags.shown = false;
}
// Propagate input events to the right elements
ctx.set_elem_events(ctx.hover_id);
ctx.set_elem_events(ctx.focus_id);
// 1. clear the tree
ctx.tree.nuke();
// send atlas updates
if (ctx.font.should_update) {
ctx.push_update_atlas(&ctx.font.atlas)!;
ctx.font.should_update = false;
}
if (ctx.sprite_atlas.should_update) {
ctx.push_update_atlas(&ctx.sprite_atlas.atlas)!;
ctx.sprite_atlas.should_update = false;
}
// send skip frame request
if (ctx.skip_frame) {
ctx.cmd_queue.push({.type = CMD_REQ_SKIP_FRAME});
}
// sort the command buffer by the z-index
// FIXME: sorting the buffer fucks with scissor commands that have to be kept in place
// TODO: instead of sorting at the end perform ordered inserts into the command buffer
//sort::countingsort(ctx.cmd_queue, fn uint(Cmd c) => c.z_index+1);
ctx.cmd_queue.sort()!;
// debug
$if $feature(DEBUG_POINTER):
// draw mouse position
Cmd cmd = {
.type = CMD_RECT,
.z_index = int.max-1, // hopefully over everything else
.rect.rect = {
.x = ctx.input.mouse.pos.x - 2,
.y = ctx.input.mouse.pos.y - 2,
.w = 4,
.h = 4,
},
.rect.color = 0xff00ffffu.to_rgba()
};
ctx.cmd_queue.push(cmd);
$endif
}
macro bool Ctx.is_hovered(&ctx, Elem *elem) => ctx.input.mouse.pos.in_rect(elem.bounds);
// Check if the element is hovered and/or focused, if it is update the context ids.
// The order in which the elements are passed to this function is not relevant
fn void Ctx.update_hover_and_focus(&ctx, Elem* elem)
{
bool hover = ctx.is_hovered(elem);
bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_ANY));
if (hover) {
Elem* prev_hover = ctx.find_elem(ctx.hover_id);
bool different = prev_hover.id != elem.id;
bool still_hovered = ctx.is_hovered(prev_hover);
bool shown = prev_hover.flags.shown;
bool above = prev_hover.z_index > elem.z_index;
hover = !(different && still_hovered && shown && above);
}
if (focus) {
Elem* prev_focus = ctx.find_elem(ctx.hover_id);
bool different = prev_focus.id != elem.id;
bool shown = prev_focus.flags.shown;
bool above = prev_focus.z_index > elem.z_index;
focus = !(different && shown && above);
}
if (hover) ctx.hover_id = elem.id;
if (focus) ctx.focus_id = elem.id;
}
// FIXME: this does not work with touch
fn void Ctx.set_elem_events(&ctx, Id id)
{
bool hover = id == ctx.hover_id;
bool focus = id == ctx.focus_id;
Elem* e = ctx.find_elem(id);
e.events = {
.has_focus = focus,
.mouse_hover = hover,
.mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY),
.mouse_release = hover && focus && ctx.is_mouse_released(BTN_ANY),
.mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY),
.key_press = focus && ctx.input.events.key_press,
.key_release = focus && ctx.input.events.key_release,
.key_repeat = focus && ctx.input.events.key_repeat,
.text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT),
};
}

227
src/font.c3 Normal file
View File

@ -0,0 +1,227 @@
module ugui::font;
import schrift;
import std::collections::map;
import std::core::mem;
import std::core::mem::allocator;
import std::io;
import std::ascii;
// ---------------------------------------------------------------------------------- //
// CODEPOINT //
// ---------------------------------------------------------------------------------- //
// unicode code point, different type for a different hash
alias Codepoint = uint;
//macro uint Codepoint.hash(self) => ((uint)self).hash();
// ---------------------------------------------------------------------------------- //
// FONT ATLAS //
// ---------------------------------------------------------------------------------- //
/* width and height of a glyph contain the kering advance
* (u,v)
* +-------------*---+ -
* | ^ | | ^
* | |oy | | |
* | v | | |
* | .ii. | | |
* | @@@@@@. | | |
* | V@Mio@@o | | |
* | :i. V@V | | h
* | :oM@@M | | |
* | :@@@MM@M | | |
* | @@o o@M | | |
* |<->:@@. M@M | | |
* |ox @@@o@@@@ | | |
* | :M@@V:@@.| | v
* +-------------*---+ -
* |<---- w ---->|
* |<------ adv ---->|
*/
struct Glyph {
Codepoint code;
ushort u, v;
ushort w, h;
short adv, ox, oy;
}
const uint FONT_CACHED = 255;
alias GlyphTable = map::HashMap{Codepoint, Glyph};
faultdef TTF_LOAD_FAILED, MISSING_GLYPH, BAD_GLYPH_METRICS, RENDER_ERROR;
struct Font {
schrift::Sft sft;
String path;
Id id; // font id, same as atlas id
GlyphTable table;
float size;
float ascender, descender, linegap; // Line Metrics
Atlas atlas;
bool should_update; // should send update_atlas command, resets at frame_end()
}
macro Rect Glyph.bounds(&g) => {.x = g.ox, .y = g.oy, .w = g.w, .h = g.h};
macro Rect Glyph.uv(&g) => {.x = g.u, .y = g.v, .w = g.w, .h = g.h};
<*
@param [&inout] font
@param [in] name
@param [&in] path
@require height > 0, scale > 0: "height and scale must be positive non-zero"
*>
fn void? Font.load(&font, Allocator allocator, String name, ZString path, uint height, float scale)
{
font.table.init(allocator, capacity: FONT_CACHED);
font.id = name.hash();
font.size = height*scale;
font.sft = {
.xScale = (double)font.size,
.yScale = (double)font.size,
.flags = schrift::SFT_DOWNWARD_Y,
};
font.sft.font = schrift::loadfile(path);
if (font.sft.font == null) {
font.table.free();
return TTF_LOAD_FAILED?;
}
schrift::SftLMetrics lmetrics;
schrift::lmetrics(&font.sft, &lmetrics);
font.ascender = (float)lmetrics.ascender;
font.descender = (float)lmetrics.descender;
font.linegap = (float)lmetrics.lineGap;
//io::printfn("ascender:%d, descender:%d, linegap:%d", font.ascender, font.descender, font.linegap);
// TODO: allocate buffer based on FONT_CACHED and the size of a sample letter
// like the letter 'A'
ushort size = (ushort)font.size*(ushort)($$sqrt((float)FONT_CACHED));
font.atlas.new(font.id, ATLAS_GRAYSCALE, size, size)!;
// preallocate the ASCII range
for (char c = ' '; c < '~'; c++) {
// FIXME: without @inline, this crashes with O1 or greater
font.get_glyph((Codepoint)c) @inline!;
}
}
<*
@param [&inout] font
*>
fn Glyph*? Font.get_glyph(&font, Codepoint code)
{
Glyph*? gp;
gp = font.table.get_ref(code);
if (catch excuse = gp) {
if (excuse != NOT_FOUND) {
return excuse?;
}
} else {
return gp;
}
// missing glyph, render and place into an atlas
Glyph glyph;
schrift::SftGlyph gid;
schrift::SftGMetrics gmtx;
if (schrift::lookup(&font.sft, (SftUChar)code, &gid) < 0) {
return MISSING_GLYPH?;
}
if (schrift::gmetrics(&font.sft, gid, &gmtx) < 0) {
return BAD_GLYPH_METRICS?;
}
schrift::SftImage img = {
.width = gmtx.minWidth,
.height = gmtx.minHeight,
};
char[] pixels = mem::new_array(char, (usz)img.width * img.height);
img.pixels = pixels;
if (schrift::render(&font.sft, gid, img) < 0) {
return RENDER_ERROR?;
}
glyph.code = code;
glyph.w = (ushort)img.width;
glyph.h = (ushort)img.height;
glyph.ox = (short)gmtx.leftSideBearing;
glyph.oy = (short)gmtx.yOffset;
glyph.adv = (short)gmtx.advanceWidth;
//io::printfn("code=%c, w=%d, h=%d, ox=%d, oy=%d, adv=%d",
// glyph.code, glyph.w, glyph.h, glyph.ox, glyph.oy, glyph.adv);
Point uv = font.atlas.place(pixels, glyph.w, glyph.h, (ushort)img.width)!;
glyph.u = uv.x;
glyph.v = uv.y;
mem::free(pixels);
font.table.set(code, glyph);
font.should_update = true;
return font.table.get_ref(code);
}
<*
@param [&inout] font
*>
fn void Font.free(&font)
{
font.atlas.free();
font.table.free();
schrift::freefont(font.sft.font);
}
// ---------------------------------------------------------------------------------- //
// FONT LOAD AND QUERY //
// ---------------------------------------------------------------------------------- //
module ugui;
<*
@param [&inout] ctx
@param [in] name
@param [&in] path
@require height > 0, scale > 0: "height and scale must be positive non-zero"
*>
fn void? Ctx.load_font(&ctx, Allocator allocator, String name, ZString path, uint height, float scale = 1.0)
{
return ctx.font.load(allocator, name, path, height, scale);
}
<*
@param [&in] ctx
@param [in] label
*>
// TODO: check if the font is present in the context
fn Id Ctx.get_font_id(&ctx, String label) => (Id)label.hash();
<*
@param [&in] ctx
@param [in] name
*>
fn Atlas*? Ctx.get_font_atlas(&ctx, String name)
{
// TODO: use the font name, for now there is only one font
if (name.hash() != ctx.font.id) {
return WRONG_ID?;
}
return &ctx.font.atlas;
}
<* @param [&in] font *>
fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5);

237
src/input.c3 Normal file
View File

@ -0,0 +1,237 @@
module ugui;
import std::io;
import std::math;
import std::core::string;
bitstruct InputEvents : uint {
bool resize : 0; // window size was changed
bool change_focus : 1; // window focus changed
bool mouse_move : 2; // mouse was moved
bool mouse_btn : 3; // mouse button pressed or released
bool mouse_scroll : 4; // mouse scroll wheel. x or y
bool text_input : 5;
bool mod_key : 6;
bool key_press : 7;
bool key_release : 8;
bool key_repeat : 9;
}
bitstruct MouseButtons : uint {
bool btn_left : 0;
bool btn_middle : 1;
bool btn_right : 2;
bool btn_4 : 3;
bool btn_5 : 4;
}
// FIXME: all of these names were prefixed with key_ idk if this is better,
// if it is remove the prefix on MouseButtons as well
// Modifier Keys, intended as any key that is not text
bitstruct ModKeys : uint {
bool lshift : 0;
bool rshift : 1;
bool lctrl : 2;
bool rctrl : 3;
bool lalt : 4;
bool ralt : 5;
bool lgui : 6;
bool rgui : 7;
bool num : 8;
bool caps : 9;
bool mode : 10;
bool scroll : 11;
bool bkspc : 12;
bool del : 13;
bool home : 14;
bool end : 15;
// arrow keys
bool up : 16;
bool down : 17;
bool left : 18;
bool right : 19;
}
const ModKeys KMOD_CTRL = {.lctrl = true, .rctrl = true};
const ModKeys KMOD_SHIFT = {.lshift = true, .rshift = true};
const ModKeys KMOD_ALT = {.lalt = true, .ralt = true};
const ModKeys KMOD_GUI = {.lgui = true, .rgui = true};
const ModKeys KMOD_TXT = {.bkspc = true, .del = true}; // modkeys that act like text input
const ModKeys KMOD_NONE = {};
const ModKeys KMOD_ANY = (ModKeys)(ModKeys.inner.max);
const MouseButtons BTN_NONE = {};
const MouseButtons BTN_ANY = (MouseButtons)(MouseButtons.inner.max);
const MouseButtons BTN_LEFT = {.btn_left = true};
const MouseButtons BTN_MIDDLE = {.btn_middle = true};
const MouseButtons BTN_RIGHT = {.btn_right = true};
const MouseButtons BTN_4 = {.btn_4 = true};
const MouseButtons BTN_5 = {.btn_5 = true};
const ModKeys KEY_ANY = (ModKeys)(ModKeys.inner.max);
<* @param [&inout] ctx *>
fn bool Ctx.check_key_combo(&ctx, ModKeys mod, String ...keys)
{
bool is_mod = (bool)(ctx.current_input.keyboard.modkeys & mod);
bool is_keys = true;
String haystack = (String)ctx.get_keys();
foreach (needle: keys) {
is_keys = is_keys && haystack.contains(needle);
}
return is_mod && is_keys;
}
// Window size was changed
<* @param [&inout] ctx *>
fn void? Ctx.input_window_size(&ctx, short width, short height)
{
if (width <= 0 || height <= 0) {
return INVALID_SIZE?;
}
ctx.current_input.events.resize = ctx.width != width || ctx.height != height;
ctx.width = width;
ctx.height = height;
if (ctx.current_input.events.resize) ctx.skip_frame = true;
}
// Window gained/lost focus
<* @param [&inout] ctx *>
fn void Ctx.input_changefocus(&ctx, bool has_focus)
{
// FIXME: raylib only has an API to query the focus status so we have to
// update the input flag only if the focus changed
ctx.current_input.events.change_focus = ctx.has_focus != has_focus;
ctx.has_focus = has_focus;
}
// NOTE: all of these refer to the previous frame's data
macro Ctx.mouse_pressed(&ctx) => ctx.input.mouse.updated & ctx.input.mouse.down;
macro Ctx.mouse_released(&ctx) => ctx.input.mouse.updated & ~ctx.input.mouse.down;
macro Ctx.mouse_down(&ctx) => ctx.input.mouse.down;
macro Ctx.is_mouse_pressed(&ctx, MouseButtons btn) => (ctx.mouse_pressed() & btn) != BTN_NONE;
macro Ctx.is_mouse_released(&ctx, MouseButtons btn) => (ctx.mouse_released() & btn) != BTN_NONE;
macro Ctx.is_mouse_down(&ctx, MouseButtons btn) => (ctx.mouse_down() & btn) != BTN_NONE;
// Mouse Buttons down
<* @param [&inout] ctx *>
fn void Ctx.input_mouse_button(&ctx, MouseButtons buttons)
{
ctx.current_input.mouse.updated = ctx.current_input.mouse.down ^ buttons;
ctx.current_input.mouse.down = buttons;
ctx.current_input.events.mouse_btn = ctx.current_input.mouse.down != BTN_NONE || ctx.current_input.mouse.updated != BTN_NONE;
}
// Mouse was moved, report absolute position
<* @param [&inout] ctx *>
fn void Ctx.input_mouse_abs(&ctx, short x, short y)
{
ctx.current_input.mouse.pos.x = math::clamp(x, (short)0, ctx.width);
ctx.current_input.mouse.pos.y = math::clamp(y, (short)0, ctx.height);
short dx, dy;
dx = x - ctx.current_input.mouse.pos.x;
dy = y - ctx.current_input.mouse.pos.y;
ctx.current_input.mouse.delta.x = dx;
ctx.current_input.mouse.delta.y = dy;
ctx.current_input.events.mouse_move = dx != 0 || dy != 0;
}
// Mouse was moved, report relative motion
<* @param [&inout] ctx *>
fn void Ctx.input_mouse_delta(&ctx, short dx, short dy)
{
ctx.current_input.mouse.delta.x = dx;
ctx.current_input.mouse.delta.y = dy;
short mx, my;
mx = ctx.current_input.mouse.pos.x + dx;
my = ctx.current_input.mouse.pos.y + dy;
ctx.current_input.mouse.pos.x = math::clamp(mx, (short)0, ctx.width);
ctx.current_input.mouse.pos.y = math::clamp(my, (short)0, ctx.height);
ctx.current_input.events.mouse_move = dx != 0 || dy != 0;
}
<* @param [&inout] ctx *>
fn void Ctx.input_mouse_wheel(&ctx, short x, short y, float scale = 1.0)
{
ctx.current_input.mouse.scroll.x = (short)((float)-x*scale);
ctx.current_input.mouse.scroll.y = (short)((float)-y*scale);
ctx.current_input.events.mouse_scroll = x !=0 || y != 0;
}
<* @param [&inout] ctx *>
fn void Ctx.input_key_press(&ctx)
{
ctx.current_input.events.key_press = true;
}
<* @param [&inout] ctx *>
fn void Ctx.input_key_release(&ctx)
{
ctx.current_input.events.key_release = true;
}
<* @param [&inout] ctx *>
fn void Ctx.input_key_repeat(&ctx)
{
ctx.current_input.events.key_repeat = true;
}
// append utf-8 encoded text to the context text input
<* @param [&inout] ctx *>
fn void Ctx.input_text_utf8(&ctx, char[] text)
{
if (text == "") return;
usz remaining = ctx.current_input.keyboard.text.len - ctx.current_input.keyboard.text_len;
usz len = text.len > remaining ? remaining : text.len;
char[] s = ctx.current_input.keyboard.text[ctx.current_input.keyboard.text_len ..];
s[..len-1] = text[..len-1];
ctx.current_input.keyboard.text_len += len;
ctx.current_input.events.text_input = true;
}
<*
@param [&inout] ctx
@param [in] text
*>
fn void? Ctx.input_text_unicode(&ctx, uint[] text)
{
if (text.len == 0) return;
usz remaining = ctx.current_input.keyboard.text.len - ctx.current_input.keyboard.text_len;
char[] s = ctx.current_input.keyboard.text[ctx.current_input.keyboard.text_len ..];
usz off = conv::utf32to8(text, s)!;
ctx.current_input.keyboard.text_len += off;
ctx.current_input.events.text_input = true;
}
<* @param [&inout] ctx *>
fn void Ctx.input_char(&ctx, char c)
{
char[1] b = {c};
ctx.input_text_utf8(b[..]);
}
fn String Ctx.get_keys(&ctx) => (String)ctx.input.keyboard.text[:ctx.input.keyboard.text_len];
fn ModKeys Ctx.get_mod(&ctx) => ctx.input.keyboard.modkeys;
// Modifier keys, like control or backspace
<* @param [&inout] ctx *>
fn void Ctx.input_mod_keys(&ctx, ModKeys modkeys, bool set)
{
if (set) {
ctx.current_input.keyboard.modkeys |= modkeys;
} else {
ctx.current_input.keyboard.modkeys &= ~modkeys;
}
ctx.current_input.events.mod_key = (uint)ctx.current_input.keyboard.modkeys != 0;
}

353
src/layout.c3 Normal file
View File

@ -0,0 +1,353 @@
module ugui;
import std::math;
import std::io;
enum LayoutDirection {
ROW,
COLUMN,
}
enum Anchor {
TOP_LEFT,
LEFT,
BOTTOM_LEFT,
BOTTOM,
BOTTOM_RIGHT,
RIGHT,
TOP_RIGHT,
TOP,
CENTER
}
struct Layout {
Size w, h; // size of the CONTENT, does not include margin, border and padding
struct children { // the children size includes the children's margin/border/pading
Size w, h;
}
TextSize text;
ushort grow_children;
short occupied;
Point origin;
Point scroll_offset;
// false: the element is laid out according to the parent
// true: the element is laid out separate from all other children and the relative position to
// the parent is the .origin field
bool absolute;
LayoutDirection dir; // the direction the children are laid out
Anchor anchor; // how the children are positioned
Rect content_offset; // combined effect of margin, border and padding
}
// Returns the width and height of a @FIT() element based on it's wanted size (min/max)
// and the content size, this function is used to both update the parent's children size and
// give the dimensions of a fit element
// TODO: test and cleanup this function
macro Point Layout.get_dimensions(&el)
{
Point dim;
// if the direction is ROW then the text is placed horizontally with the children
if (el.dir == ROW) {
Size content_width = el.children.w + el.text.width;
Size width = el.w.combine(content_width);
short final_width = width.greater();
short text_height;
if (el.text.area != 0) {
short text_width = (@exact(final_width) - el.children.w).combine(el.text.width).min;
text_height = @exact((short)(el.text.area / text_width)).combine(el.text.height).min;
}
Size content_height = el.children.h.comb_max(@exact(text_height));
Size height = el.h.combine(content_height);
short final_height = height.greater();
dim = {
.x = final_width + el.content_offset.x + el.content_offset.w,
.y = final_height + el.content_offset.y + el.content_offset.h,
};
} else {
// if the direction is COLUMN the text and children are one on top of the other
Size content_width = el.children.w.comb_max(el.text.width);
Size width = el.w.combine(content_width);
short final_width = width.greater();
short text_height;
if (el.text.area != 0) {
short text_width = @exact(final_width).combine(el.text.width).min;
text_height = @exact((short)(el.text.area / text_width)).combine(el.text.height).min;
}
Size content_height = el.children.h + @exact(text_height);
Size height = el.h.combine(content_height);
short final_height = height.greater();
dim = {
.x = final_width + el.content_offset.x + el.content_offset.w,
.y = final_height + el.content_offset.y + el.content_offset.h,
};
}
// GROSS HACK FOR EXACT DIMENSIONS
if (el.w.@is_exact()) dim.x = el.w.min + el.content_offset.x + el.content_offset.w;
if (el.h.@is_exact()) dim.y = el.h.min + el.content_offset.y + el.content_offset.h;
// GROSS HACK FOR GROW DIMENSIONS
// FIXME: does this always work?
if (el.w.@is_grow()) dim.x = 0;
if (el.h.@is_grow()) dim.y = 0;
return dim;
}
// The content space of the element
macro Point Elem.content_space(&e)
{
return {
.x = (short)max(e.bounds.w - e.layout.content_offset.x - e.layout.content_offset.w, 0),
.y = (short)max(e.bounds.h - e.layout.content_offset.y - e.layout.content_offset.h, 0),
};
}
// Update the parent element's children size
fn void update_parent_size(Elem* child, Elem* parent)
{
Layout* cl = &child.layout;
Layout* pl = &parent.layout;
// if the element has absolute position do not update the parent
if (cl.absolute) return;
Point child_size = cl.get_dimensions();
switch (pl.dir) {
case ROW: // on rows grow the ch width by the child width and only grow ch height if it exceeds
pl.children.w += @exact(child_size.x);
pl.children.h = pl.children.h.comb_max(@exact(child_size.y));
if (child.layout.w.@is_grow()) parent.layout.grow_children++;
case COLUMN: // do the opposite on column
pl.children.w = pl.children.w.comb_max(@exact(child_size.x));
pl.children.h += @exact(child_size.y);
if (child.layout.h.@is_grow()) parent.layout.grow_children++;
}
}
macro Rect Elem.content_bounds(&elem) => elem.bounds.pad(elem.layout.content_offset);
// Assign the width and height of an element in the directions that it doesn't need to grow
fn void resolve_dimensions(Elem* e, Elem* p)
{
Layout* el = &e.layout;
Layout* pl = &p.layout;
Point elem_dimensions = el.get_dimensions();
e.bounds.w = elem_dimensions.x;
e.bounds.h = elem_dimensions.y;
// if the element has absolute position do not update the parent
if (el.absolute) return;
switch (pl.dir) {
case ROW:
if (!el.w.@is_grow()) pl.occupied += e.bounds.w;
case COLUMN:
if (!el.h.@is_grow()) pl.occupied += e.bounds.h;
}
}
fn void resolve_grow_elements(Elem* e, Elem* p)
{
// WIDTH
if (e.layout.w.@is_grow()) {
if (e.layout.absolute) { // absolute children do not need to share space
e.bounds.w = p.content_space().x;
} else if (p.layout.dir == ROW) { // grow along the axis, divide the parent size
short slot = (short)max(((p.content_space().x - p.layout.occupied) / p.layout.grow_children), 0);
e.bounds.w = slot;
p.layout.grow_children--;
p.layout.occupied += slot;
} else if (p.layout.dir == COLUMN) { // grow across the layout axis, inherit width of the parent
e.bounds.w = p.content_space().x;
}
}
// HEIGHT
if (e.layout.h.@is_grow()) {
if (e.layout.absolute) { // absolute children do not need to share space
e.bounds.h = p.content_space().y;
} else if (p.layout.dir == COLUMN) { // grow along the axis, divide the parent size
short slot = (short)max(((p.content_space().y - p.layout.occupied) / p.layout.grow_children), 0);
e.bounds.h = slot;
p.layout.grow_children--;
p.layout.occupied += slot;
} else if (p.layout.dir == ROW) { // grow across the layout axis, inherit width of the parent
e.bounds.h = p.content_space().y;
}
}
}
fn void resolve_placement(Elem* c, Elem* p)
{
Layout* pl = &p.layout;
Layout* cl = &c.layout;
Point off = {
.x = p.bounds.x + pl.origin.x + pl.content_offset.x,
.y = p.bounds.y + pl.origin.y + pl.content_offset.y,
};
// if the element has absolute position assign the origin and do not update the parent
if (cl.absolute) {
c.bounds.x = p.bounds.x + pl.content_offset.x + cl.origin.x;
c.bounds.y = p.bounds.y + pl.content_offset.y + cl.origin.y;
return;
}
switch (pl.anchor) {
case TOP_LEFT:
c.bounds.x = off.x;
c.bounds.y = off.y;
case LEFT:
c.bounds.x = off.x;
c.bounds.y = off.y + p.content_space().y/2;
if (pl.dir == COLUMN) {
c.bounds.y -= pl.occupied/2;
} else if (pl.dir == ROW) {
c.bounds.y -= c.bounds.h/2;
}
case BOTTOM_LEFT:
c.bounds.x = off.x;
c.bounds.y = off.y + p.content_space().y ;
if (pl.dir == COLUMN) {
c.bounds.y -= pl.occupied;
} else if (pl.dir == ROW) {
c.bounds.y -= c.bounds.h;
}
case BOTTOM:
c.bounds.x = off.x + p.content_space().x/2;
c.bounds.y = off.y + p.content_space().y;
if (pl.dir == COLUMN) {
c.bounds.y -= pl.occupied;
c.bounds.x -= c.bounds.w/2;
} else if (pl.dir == ROW) {
c.bounds.y -= c.bounds.h;
c.bounds.x -= pl.occupied/2;
}
case BOTTOM_RIGHT:
c.bounds.x = off.x + p.content_space().x;
c.bounds.y = off.y + p.content_space().y;
if (pl.dir == COLUMN) {
c.bounds.y -= pl.occupied;
c.bounds.x -= c.bounds.w;
} else if (pl.dir == ROW) {
c.bounds.y -= c.bounds.h;
c.bounds.x -= pl.occupied;
}
case RIGHT:
c.bounds.x = off.x + p.content_space().x;
c.bounds.y = off.y + p.content_space().y/2;
if (pl.dir == COLUMN) {
c.bounds.y -= pl.occupied/2;
c.bounds.x -= c.bounds.w;
} else if (pl.dir == ROW) {
c.bounds.y -= c.bounds.h/2;
c.bounds.x -= pl.occupied;
}
case TOP_RIGHT:
c.bounds.x = off.x + p.content_space().x;
c.bounds.y = off.y;
if (pl.dir == COLUMN) {
c.bounds.x -= c.bounds.w;
} else if (pl.dir == ROW) {
c.bounds.x -= pl.occupied;
}
case TOP:
c.bounds.x = off.x + p.content_space().x/2;
c.bounds.y = off.y;
if (pl.dir == COLUMN) {
c.bounds.x -= c.bounds.w/2;
} else if (pl.dir == ROW) {
c.bounds.x -= pl.occupied/2;
}
case CENTER:
c.bounds.x = off.x + p.content_space().x/2;
c.bounds.y = off.y + p.content_space().y/2;
if (pl.dir == COLUMN) {
c.bounds.x -= c.bounds.w/2;
c.bounds.y -= pl.occupied/2;
} else if (pl.dir == ROW) {
c.bounds.x -= pl.occupied/2;
c.bounds.y -= c.bounds.h/2;
}
break;
}
switch (pl.dir) {
case ROW:
pl.origin.x += c.bounds.w;
case COLUMN:
pl.origin.y += c.bounds.h;
default: unreachable("unknown layout direction");
}
// update the parent children_bounds
// FIXME: this causes scollbars to flicker in/out during resize because the current frames children_bounds are updated
// with the previous' frame child bounds. It would be better to implement another pass during layout.
// FIXME: this long way around to compute the children bounds works and reduces flickering, but it is very ugly
Rect ncb = c.children_bounds;
ncb.x = c.bounds.x;
ncb.y = c.bounds.y;
Rect cb = containing_rect(c.bounds, ncb);
Point o = p.layout.scroll_offset;
p.children_bounds = containing_rect(cb + o, p.children_bounds);
// reset the children bounds
c.children_bounds = {
.x = c.bounds.x,
.y = c.bounds.y
};
}
fn void? Ctx.layout_element_tree(&ctx)
{
int current;
for (int n; (current = ctx.tree.level_order_it(0, n)) >= 0; n++) {
Elem* p = ctx.find_elem(ctx.tree.get(current))!;
p.layout.origin = -p.layout.scroll_offset;
int ch;
// RESOLVE KNOWN DIMENSIONS
for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) {
Elem* c = ctx.find_elem(ctx.tree.get(ch))!;
if (ctx.tree.is_root(ch)) {
resolve_dimensions(p, &&{});
} else {
resolve_dimensions(c, p);
}
}
// RESOLVE GROW CHILDREN
for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) {
Elem* c = ctx.find_elem(ctx.tree.get(ch))!;
if (ctx.tree.is_root(ch)) {
resolve_grow_elements(p, &&{});
} else {
resolve_grow_elements(c, p);
}
}
// RESOLVE CHILDREN PLACEMENT
for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) {
Elem* c = ctx.find_elem(ctx.tree.get(ch))!;
if (ctx.tree.is_root(ch)) {
resolve_placement(p, &&{});
} else {
resolve_placement(c, p);
}
// FIXME: this stuff would be better elsewhere but we are already iteraring through all
// elements so here it fits really well
ctx.update_hover_and_focus(c);
}
}
}

367
src/mtree.c3 Normal file
View File

@ -0,0 +1,367 @@
module mtree{Type};
/* ================================================================================================
* MTree, Bitmap-based tree
* ================================================================================================
*
* Overview
* --------
* The MTree is a bitmap-based tree structure composed of three core elements:
* - Element Vector: Stores user data.
* - Reference Node Vector: Manages node relationships.
* - Bitmap: Marks used indices.
*
* The name "MTree" originates from "Matrix Tree," where the vector is divided into
* sectors of power-of-two sizes. Each node's bitmap marks the positions of its
* children within the same sector.
*
* If a parent and its children are in different sectors, a new node is created.
* The parent's "next" field points to this new node, forming a chain that must
* be traversed during iteration.
*
*
* Example (sector size = 8)
* -------------------------
*
* _________________________________
* |__ __ _______________________ |
* | | | | _ |
* | v v vv |v
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
* refs_vec:| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|13|14|15|16|...
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
* \__________ __________/ \__________ __________/ \__
* V V
* sector 0 sector 1
*
*
* Node Relationships:
* -------------------
* - Root (Element 0) has three direct children: 1, 2, and 10.
* - Node 10 is in a different sector than the root, so root.next points to Node 11.
* - Node 11 has Node 10 as a direct child and Node 0 (root) as its parent.
*
* Bitmap Representation:
* ---------------------
*
* root = {
* .parent = -1; // Root has no parent
* .next = 11; // Points to Node 11
* .children = 0b00000110; // [0|1|1|0|0|0|0|0] (Children: 1, 2)
* }
*
* node11 = {
* .parent = 0; // Parent is root (Node 0)
* .next = -1; // Last in the chain
* .children = 0b00000100; // [0|0|1|0|0|0|0|0] (Child: 10)
* }
*
* ================================================================================================
*/
import std::core::mem;
import std::core::mem::allocator;
import std::io;
import std::bits;
import std::collections::list;
alias Bitmap = ulong;
const BITS = Bitmap.sizeof*8;
alias IdxList = List{int};
// next: if positive it contains the index of the next node that contains the children information
struct RefNode {
int next;
int parent;
Bitmap children;
}
struct MTree {
usz elements;
Allocator allocator;
IdxList queue;
Bitmap[] used;
Type[] elem_vec; // element vector
RefNode[] refs_vec; // relationship vector
}
<* @param [&inout] tree *>
fn void MTree.init(&tree, usz size, Allocator allocator = mem)
{
// round size to the nearest multiple of BITS
size = size + size%BITS;
tree.elements = 0;
tree.allocator = allocator;
tree.queue.init(tree.allocator, size);
tree.used = allocator::new_array(tree.allocator, Bitmap, size/BITS);
tree.elem_vec = allocator::new_array(tree.allocator, Type, size);
tree.refs_vec = allocator::new_array(tree.allocator, RefNode, size);
foreach (&r: tree.refs_vec) {
r.next = -1;
}
}
<* @param [&inout] tree *>
fn void MTree.free(&tree)
{
tree.elements = 0;
tree.queue.free();
(void)allocator::free(tree.allocator, tree.used);
(void)allocator::free(tree.allocator, tree.elem_vec);
(void)allocator::free(tree.allocator, tree.refs_vec);
}
<* @param [&inout] tree *>
fn int? MTree.get_free_spot(&tree)
{
foreach (idx, d: tree.used) {
if (d != $typeof(d).max) {
int spot = (int)idx*BITS + BITS-(int)d.clz();
return spot;
}
}
return CAPACITY_EXCEEDED?;
}
<* @require idx >= 0 *>
macro void MTree.set_used(&tree, int idx)
{
int r = idx % BITS;
int q = idx / BITS;
tree.used[q] |= (1l << r);
}
<* @require idx >= 0 *>
macro void MTree.unset_used(&tree, int idx)
{
int r = idx % BITS;
int q = idx / BITS;
tree.used[q] &= ~(1l << r);
}
<* @require idx >= 0 *>
macro bool MTree.is_used(&tree, int idx)
{
int r = idx % BITS;
int q = idx / BITS;
return !!(tree.used[q] & (1l << r));
}
// get the last node in the "next" chain
<* @require tree.is_used(parent) == true *>
fn int MTree.last_node(&tree, int parent)
{
while(tree.refs_vec[parent].next >= 0) {
parent = tree.refs_vec[parent].next;
}
return parent;
}
<*
@require tree.elements == 0 || tree.is_used(parent) == true
@param [&inout] tree
*>
fn int? MTree.add(&tree, int parent, Type t)
{
int idx = tree.get_free_spot()!;
int subtree = idx / BITS;
tree.set_used(idx);
tree.elem_vec[idx] = t;
tree.refs_vec[idx] = (RefNode){
.parent = parent,
.next = -1,
};
tree.elements++;
// root element, has no parent
if (tree.elements == 1) {
tree.refs_vec[idx].parent = -1;
return idx;
}
// if the parent already has a node in the same subtree as the child then update that node's
// children bitmap
bool done;
for (int p = parent; p >= 0; p = tree.refs_vec[p].next) {
int ps = p/BITS;
if (ps == subtree) {
tree.refs_vec[p].children |= (1l << (idx%BITS));
done = true;
break;
}
}
// on fail we need to create another parent node
if (!done) {
int new_next = tree.get_free_spot()!;
// if the new node does not land in the same subtree as the child we cannot do
// anything since the references are immutable
if (new_next/BITS != subtree) {
return CAPACITY_EXCEEDED?;
}
tree.set_used(new_next);
tree.elements++;
// update the "next" chain
int last_link = tree.last_node(parent);
tree.refs_vec[last_link].next = new_next;
tree.refs_vec[new_next].next = -1;
tree.refs_vec[new_next].children |= (long)(1 << (idx%BITS));
tree.refs_vec[new_next].parent = last_link;
// FIXME: the elem_vec is not updated, do we need to?
}
return idx;
}
// get the index of the n-th children of parent, -1 otherwise
// usage: for (int i, c; (c = tree.children_it(parent, i)) >= 0; i++) { ... }
<* @param [&in] tree *>
fn int MTree.children_it(&tree, int parent, int n)
{
int tot_children;
int child;
for (int p = parent; p >= 0; p = tree.refs_vec[p].next) {
int cn = (int)tree.refs_vec[p].children.popcount();
tot_children += cn;
// we are in the right subtree
if (tot_children > n) {
child = (p/BITS) * BITS; // start at the parent's subtree index
int j = cn - (tot_children - n); // we need the j-th children of this node
Bitmap u = tree.refs_vec[p].children;
child += j; // add the children number
do {
child += (int)u.ctz(); // increment by the skipped zeroes
u >>= u.ctz() + 1;
j--;
} while (j >= 0);
return child;
}
}
return -1;
}
<* @param [&in] tree *>
fn int MTree.children_num(&tree, int parent)
{
int n;
for (int p = parent; p >= 0; p = tree.refs_vec[p].next) {
n += (int)tree.refs_vec[p].children.popcount();
}
return n;
}
<* @param [&in] tree *>
fn int MTree.subtree_size(&tree, int parent)
{
int x = tree.children_num(parent);
int c;
for (int n; (c = tree.children_it(parent, n)) >= 0; n++) {
x += tree.subtree_size(c);
}
return x;
}
<* @param [&inout] tree *>
fn int MTree.level_order_it(&tree, int parent, int i)
{
if (i == 0) {
tree.queue.clear();
tree.queue.push(parent);
}
if (tree.queue.len() == 0) return -1;
int p = tree.queue.pop_first()!!;
int c;
for (int n; (c = tree.children_it(p, n)) >= 0; n++) {
tree.queue.push(c);
}
return p;
}
<* @param [&inout] tree *>
fn void MTree.prune(&tree, int parent)
{
if (!tree.is_used(parent)) return;
int c;
for (int i = 0; (c = tree.children_it(parent, i)) >= 0; i++) {
tree.prune(c); // prune the subtree
// delete all children including their next chain
for (int p = c; p >= 0;) {
int next = tree.refs_vec[p].next;
tree.unset_used(p);
tree.refs_vec[p] = {.next = -1};
p = next;
}
}
// finally delete the parent
for (int p = parent; p >= 0;) {
int next = tree.refs_vec[p].next;
tree.unset_used(p);
tree.elements--;
tree.refs_vec[p] = {.next = -1};
p = next;
}
}
<*
@require ref >= 0 , ref < tree.elem_vec.len
@param [&inout] tree
*>
fn Type? MTree.get(&tree, int ref) @operator([])
{
if (tree.is_used(ref)) return tree.elem_vec[ref];
return NOT_FOUND?;
}
<* @param [&in] tree *>
fn Type? MTree.parentof(&tree, int ref)
{
if (!tree.is_used(ref)) return NOT_FOUND?;
return tree.refs_vec[ref].parent;
}
<* @param [&inout] tree *>
fn void MTree.nuke(&tree)
{
foreach (idx, &b: tree.used) {
*b = 0;
tree.refs_vec[idx] = {.next = -1};
}
tree.elements = 0;
}
<* @param [&in] t *>
macro bool MTree.is_root(&t, int i) => t.is_used(i) && t.refs_vec[i].parent == -1;
<* @param [&in] tree *>
fn void MTree.print(&tree)
{
foreach (idx, c: tree.elem_vec) {
if (tree.is_used((int)idx)) {
io::printfn("[%d](%s) parent:%d next:%d children:%b",
idx, c, tree.refs_vec[idx].parent, tree.refs_vec[idx].next,
tree.refs_vec[idx].children
);
}
}
}

268
src/shapes.c3 Normal file
View File

@ -0,0 +1,268 @@
module ugui;
// ---------------------------------------------------------------------------------- //
// RECTANGLE //
// ---------------------------------------------------------------------------------- //
// Rect and it's methods
struct Rect {
short x, y, w, h;
}
// TODO: find another name
const Rect RECT_MAX = {0, 0, short.max, short.max};
// return true if rect a contains b
macro bool Rect.contains(Rect a, Rect b)
{
return (a.x <= b.x && a.y <= b.y && a.x+a.w >= b.x+b.w && a.y+a.h >= b.y+b.h);
}
// returns the intersection of a and b
macro Rect Rect.intersection(Rect a, Rect b)
{
return {
.x = (short)max(a.x, b.x),
.y = (short)max(a.y, b.y),
.w = (short)min(a.x+a.w, b.x+b.w) - (short)max(a.x, b.x),
.h = (short)min(a.y+a.h, b.y+b.h) - (short)max(a.y, b.y),
};
}
// returns true if the intersection not null
macro bool Rect.collides(Rect a, Rect b)
{
return !(a.x > b.x+b.w || a.x+a.w < b.x || a.y > b.y+b.h || a.y+a.h < b.y);
}
// return a rect that contains both rects, a bounding box of both
macro Rect containing_rect(Rect a, Rect b)
{
short min_x = (short)min(a.x, b.x);
short min_y = (short)min(a.y, b.y);
short max_x = (short)max(a.x + a.w, b.x + b.w);
short max_y = (short)max(a.y + a.h, b.y + b.h);
return {
.x = min_x,
.y = min_y,
.w = (short)(max_x - min_x),
.h = (short)(max_y - min_y)
};
}
// check for empty rect
macro bool Rect.is_null(Rect r) => r.x == 0 && r.y == 0 && r.x == 0 && r.w == 0;
// returns the element-wise addition of r1 and r2
macro Rect Rect.add(Rect r1, Rect r2) @operator_s(+)
{
return {
.x = r1.x + r2.x,
.y = r1.y + r2.y,
.w = r1.w + r2.w,
.h = r1.h + r2.h,
};
}
// returns the element-wise subtraction of r1 and r2
macro Rect Rect.sub(Rect r1, Rect r2) @operator_s(-)
{
return {
.x = r1.x - r2.x,
.y = r1.y - r2.y,
.w = r1.w - r2.w,
.h = r1.h - r2.h,
};
}
// returns the element-wise multiplication of r1 and r2
macro Rect Rect.mul(Rect r1, Rect r2) @operator_s(*)
{
return {
.x = r1.x * r2.x,
.y = r1.y * r2.y,
.w = r1.w * r2.w,
.h = r1.h * r2.h,
};
}
macro Point Rect.position(Rect r)
{
return {
.x = r.x,
.y = r.y,
};
}
macro Point Rect.size(Rect r)
{
return {
.x = r.w,
.y = r.h,
};
}
macro Rect Rect.max(Rect a, Rect b)
{
return {
.x = max(a.x, b.x),
.y = max(a.y, b.y),
.w = max(a.w, b.w),
.h = max(a.h, b.h),
};
}
macro Rect Rect.min(Rect a, Rect b)
{
return {
.x = min(a.x, b.x),
.y = min(a.y, b.y),
.w = min(a.w, b.w),
.h = min(a.h, b.h),
};
}
// Offset a rect by a point
macro Rect Rect.off(Rect r, Point p) @operator_s(+)
{
return {
.x = r.x + p.x,
.y = r.y + p.y,
.w = r.w,
.h = r.h,
};
}
// Resize a rect width and height
macro Rect Rect.grow(Rect r, Point p)
{
return {
.x = r.x,
.y = r.y,
.w = r.w + p.x,
.h = r.h + p.y,
};
}
// Return the bottom-right corner of a rectangle
macro Point Rect.bottom_right(Rect r)
{
return {
.x = r.x + r.w,
.y = r.y + r.h,
};
}
macro Rect Rect.center_to(Rect a, Rect b)
{
return {
.x = b.x + (b.w - a.w)/2,
.y = b.y + (b.h - a.h)/2,
.w = a.w,
.h = a.h,
};
}
macro Rect Rect.pad(Rect a, Rect b)
{
return {
.x = a.x + b.x,
.y = a.y + b.y,
.w = a.w - b.x - b.w,
.h = a.h - b.y - b.h,
};
}
macro Rect Rect.expand(Rect a, Rect b)
{
return {
.x = a.x - b.x,
.y = a.y - b.y,
.w = a.w + b.x + b.w,
.h = a.h + b.y + b.h,
};
}
// ---------------------------------------------------------------------------------- //
// POINT //
// ---------------------------------------------------------------------------------- //
struct Point {
short x, y;
}
// returns true if a point is inside the rectangle
macro bool Point.in_rect(Point p, Rect r)
{
return (p.x >= r.x && p.x <= r.x + r.w) && (p.y >= r.y && p.y <= r.y + r.h);
}
macro bool Point.outside(Point p, Rect r) => !p.in_rect(r);
macro Point Point.add(Point a, Point b) @operator_s(+) => {.x = a.x+b.x, .y = a.y+b.y};
macro Point Point.sub(Point a, Point b) @operator_s(-) => {.x = a.x-b.x, .y = a.y-b.y};
macro Point Point.neg(Point p) @operator_s(-) => {-p.x, -p.y};
macro Point Point.max(Point a, Point b) => {.x = max(a.x, b.x), .y = max(a.y, b.y)};
macro Point Point.min(Point a, Point b) => {.x = min(a.x, b.x), .y = min(a.y, b.y)};
macro bool Point.equals(Point a, Point b) @operator_s(==) => a.x == b.x && a.y == b.y;
// ---------------------------------------------------------------------------------- //
// COLOR //
// ---------------------------------------------------------------------------------- //
struct Color{
char r, g, b, a;
}
macro Color uint.to_rgba(u)
{
return {
.r = (char)((u >> 24) & 0xff),
.g = (char)((u >> 16) & 0xff),
.b = (char)((u >> 8) & 0xff),
.a = (char)((u >> 0) & 0xff)
};
}
macro Color uint.@to_rgba($u)
{
return {
.r = (char)(($u >> 24) & 0xff),
.g = (char)(($u >> 16) & 0xff),
.b = (char)(($u >> 8) & 0xff),
.a = (char)(($u >> 0) & 0xff)
};
}
macro uint Color.to_uint(c) => c.r | (c.g << 8) | (c.b << 16) | (c.a << 24);
// ---------------------------------------------------------------------------------- //
// SIZE //
// ---------------------------------------------------------------------------------- //
macro short short.add_no_of(short a, short b) => (short)max(min((int)a + (int)b, short.max), short.min) @inline;
struct Size {
short min, max;
}
macro Size @grow() => {.min = 0, .max = 0};
macro Size @exact(short s) => {.min = s, .max = s};
macro Size @fit(short min = 0, short max = short.max) => {.min = min, .max = max};
macro bool Size.@is_grow(s) => (s.min == 0 && s.max == 0);
macro bool Size.@is_exact(s) => (s.min == s.max && s.min != 0);
macro bool Size.@is_fit(s) => (s.min != s.max);
macro Size Size.add(a, Size b) @operator_s(+) => {.min = a.min.add_no_of(b.min), .max = a.max.add_no_of(b.max)};
macro Size Size.sub(a, Size b) @operator_s(-) => {.min = a.min.add_no_of(-b.min), .max = a.max.add_no_of(-b.max)};
macro Size Size.combine(a, Size b) => {.min = max(a.min, b.min), .max = min(a.max, b.max)};
macro Size Size.comb_max(a, Size b) => {.min = max(a.min, b.min), .max = max(a.max, b.max)};
macro Size Size.comb_min(a, Size b) => {.min = min(a.min, b.min), .max = min(a.max, b.max)};
macro short Size.greater(a) => a.min > a.max ? a.min : a.max;

107
src/sprite.c3 Normal file
View File

@ -0,0 +1,107 @@
module ugui;
import std::core::mem::allocator;
import std::collections::map;
import std::io;
import std::compression::qoi;
const usz SRITES_PER_ATLAS = 64;
enum SpriteType {
SPRITE_NORMAL,
SPRITE_SDF,
SPRITE_MSDF,
SPRITE_ANIMATED,
}
struct Sprite {
Id id;
SpriteType type;
ushort u, v;
ushort w, h;
}
alias SpriteMap = map::HashMap{Id, Sprite};
struct SpriteAtlas {
Id id;
Atlas atlas;
SpriteMap sprites;
bool should_update;
}
// name: some examples are "icons" or "images"
fn void? SpriteAtlas.init(&this, String name, AtlasType type, ushort width, ushort height)
{
// FIXME: for now only R8G8B8A8 format is supported
if (type != ATLAS_R8G8B8A8) {
return INVALID_TYPE?;
}
this.id = name.hash();
this.atlas.new(this.id, AtlasType.ATLAS_R8G8B8A8, width, height)!;
this.sprites.init(allocator::mem, capacity: SRITES_PER_ATLAS);
this.should_update = false;
}
fn void? SpriteAtlas.free(&this)
{
this.atlas.free();
this.sprites.free();
}
// FIXME: this should throw an error when a different pixel format than the atlas' is used
// or convert from the source's pixel format to the atlas'
fn Sprite*? SpriteAtlas.insert(&this, String name, SpriteType type, char[] pixels, ushort w, ushort h, ushort stride)
{
Sprite s;
s.id = name.hash();
s.type = type;
Point uv = this.atlas.place(pixels, w, h, stride)!;
s.w = w;
s.h = h;
s.u = uv.x;
s.v = uv.y;
this.sprites.set(s.id, s);
this.should_update = true;
return this.sprites.get_ref(s.id);
}
fn Sprite*? SpriteAtlas.get(&this, String name)
{
Id id = name.hash();
return this.sprites.get_ref(id);
}
fn Sprite*? SpriteAtlas.get_by_id(&this, Id id)
{
return this.sprites.get_ref(id);
}
macro Rect Sprite.rect(s) => {0,0,s.w,s.h};
macro Rect Sprite.uv(s) => {s.u,s.v,s.w,s.h};
fn void? Ctx.sprite_atlas_create(&ctx, String name, AtlasType type, ushort w, ushort h)
{
ctx.sprite_atlas.init(name, type, w, h)!;
}
fn Id Ctx.get_sprite_atlas_id(&ctx, String name)
{
return name.hash();
}
fn void? Ctx.import_sprite_memory(&ctx, String name, char[] pixels, ushort w, ushort h, ushort stride, SpriteType type = SPRITE_NORMAL)
{
ctx.sprite_atlas.insert(name, type, pixels, w, h, stride)!;
}
fn void? Ctx.import_sprite_file_qoi(&ctx, String name, String path, SpriteType type = SPRITE_NORMAL)
{
QOIDesc desc;
char[] pixels = qoi::read(allocator::mem, path, &desc, QOIChannels.RGBA)!;
defer mem::free(pixels);
ctx.sprite_atlas.insert(name, type, pixels, (ushort)desc.width, (ushort)desc.height, (ushort)desc.width)!;
}

588
src/string.c3 Normal file
View File

@ -0,0 +1,588 @@
module ugui;
import std::collections::list;
import std::core::string;
struct LineInfo @local {
usz start, end;
short width, height;
short first_off; // first character offset
}
macro usz LineInfo.len(li) => li.end-li.start;
alias LineStack @local = list::List{LineInfo};
fn short Rect.y_off(Rect bounds, short height, Anchor anchor) @local
{
short off;
switch (anchor) {
case TOP_LEFT: nextcase;
case TOP: nextcase;
case TOP_RIGHT:
off = 0;
case LEFT: nextcase;
case CENTER: nextcase;
case RIGHT:
off = (short)(bounds.h - height)/2;
case BOTTOM_LEFT: nextcase;
case BOTTOM: nextcase;
case BOTTOM_RIGHT:
off = (short)(bounds.h - height);
}
return off;
}
fn short Rect.x_off(Rect bounds, short width, Anchor anchor) @local
{
short off;
switch (anchor) {
case TOP_LEFT: nextcase;
case LEFT: nextcase;
case BOTTOM_LEFT:
off = 0;
case TOP: nextcase;
case CENTER: nextcase;
case BOTTOM:
off = (short)(bounds.w - width)/2;
case TOP_RIGHT: nextcase;
case RIGHT: nextcase;
case BOTTOM_RIGHT:
off = (short)(bounds.w - width);
}
return off;
}
// ---------------------------------------------------------------------------------- //
// STRING LAYOUT //
// ---------------------------------------------------------------------------------- //
struct GlyphIterator {
short baseline;
short line_height;
short line_gap;
short space_width;
short tab_width;
Rect bounds;
Anchor anchor;
bool reflow;
Font* font;
LineStack lines;
usz line_off, line_idx;
String text;
Codepoint cp;
Glyph* gp;
short adv; // prev advance
Point o;
Rect str_bounds;
}
<*
@param [&inout] self
@param [in] text
@param [&inout] font
*>
fn void? GlyphIterator.init(&self, Allocator allocator, String text, Rect bounds, Font* font, Anchor anchor, bool reflow, uint tab_size)
{
self.font = font;
self.line_height = (short)font.line_height();
self.baseline = (short)font.ascender;
self.line_gap = (short)font.linegap;
self.space_width = font.get_glyph(' ').adv!;
self.tab_width = self.space_width * (short)tab_size;
self.bounds = bounds;
self.o = bounds.position();
self.reflow = reflow;
self.text = text;
self.anchor = anchor;
// if the anchor is top_left we can skip dividing the string line by line, in GlyphIterator.next
// this has to be accounted for
if (anchor != TOP_LEFT) {
self.lines.init(allocator, 4);
self.populate_lines_stack()!;
self.line_off = 0;
self.line_idx = 0;
if (self.lines.len() > 0) {
self.o.y += bounds.y_off(self.str_bounds.h, anchor);
self.o.x += bounds.x_off(self.lines[0].width, anchor) - self.lines[0].first_off;
}
}
}
fn void? GlyphIterator.populate_lines_stack(&self)
{
usz line_start;
LineInfo li;
Point o = self.o;
StringIterator ti = self.text.iterator();
usz prev_off;
for (Codepoint cp; ti.has_next();) {
cp = ti.next()!;
usz off = ti.current;
bool push = false;
li.height = self.line_height;
switch {
case cp == '\n':
push = true;
case cp == '\t':
o.x += self.tab_width;
case ascii::is_cntrl((char)cp):
break;
default:
Glyph* gp = self.font.get_glyph(cp)!;
if (off == line_start) {
li.first_off = gp.ox;
o.x -= gp.ox;
}
Rect b = gp.bounds().off(o);
b.y += self.baseline;
if (self.reflow && b.x + b.w > self.bounds.x + self.bounds.w) {
push = true;
// roll back this character since it is on the next line
ti.current = prev_off;
off = prev_off;
} else {
o.x += gp.adv;
li.width += gp.adv;
}
}
if (push) {
li.start = line_start;
li.end = off;
self.lines.push(li);
self.str_bounds.w = max(self.str_bounds.w, li.width);
self.str_bounds.h += li.height;
o.x = self.bounds.x;
o.y += self.line_height;
line_start = off;
li.height = 0;
li.width = 0;
}
prev_off = off;
}
if (line_start != ti.current) {
// FIXME: crap, can we not repeat this code?
li.start = line_start;
li.end = ti.current;
self.lines.push(li);
self.str_bounds.w = max(self.str_bounds.w, li.width);
self.str_bounds.h += li.height;
}
self.str_bounds.h += (short)(self.lines.len()-1) * self.line_gap;
}
fn String GlyphIterator.current_line(&self)
{
LineInfo li = self.lines[self.line_idx];
return self.text[li.start:li.len()];
}
fn Rect? GlyphIterator.next(&self)
{
// check if there is a next glyph and maybe update the line and offset indices
if (self.anchor != TOP_LEFT) {
if (self.line_idx >= self.lines.len()) {
return NO_MORE_ELEMENT?;
}
LineInfo li = self.lines[self.line_idx];
if (self.line_off >= li.len()) {
self.line_idx++;
if (self.line_idx >= self.lines.len()) {
return NO_MORE_ELEMENT?;
}
self.line_off = 0;
li = self.lines[self.line_idx];
self.o.y += self.line_height + self.line_gap;
self.o.x = self.bounds.x + self.bounds.x_off(li.width, self.anchor) - li.first_off;
}
} else if (self.line_off >= self.text.len) {
return NO_MORE_ELEMENT?;
}
String t;
if (self.anchor != TOP_LEFT) {
t = self.current_line()[self.line_off..];
} else {
t = self.text[self.line_off..];
}
usz read = t.len < 4 ? t.len : 4;
self.cp = conv::utf8_to_char32(&t[0], &read)!;
self.line_off += read;
self.gp = self.font.get_glyph(self.cp)!;
Rect b = {.x = self.o.x, .y = self.o.y};
Point prev_o = self.o;
self.adv = 0;
switch {
case self.cp == '\n':
if (self.anchor == TOP_LEFT) {
self.o.x = self.bounds.x;
self.o.y += self.line_height + self.line_gap;
self.line_idx++;
}
break;
case self.cp == '\t':
self.o.x += self.tab_width;
case ascii::is_cntrl((char)self.cp):
break;
default:
b = self.gp.bounds().off(self.o);
b.y += self.baseline;
if (self.anchor == TOP_LEFT) {
//if (self.o.x == self.bounds.x) self.bounds.x -= self.gp.ox;
if (self.reflow && b.bottom_right().x > self.bounds.bottom_right().x) {
self.o.x = self.bounds.x - self.gp.ox;
self.o.y += self.line_height + self.line_gap;
self.line_idx++;
b = self.gp.bounds().off(self.o);
b.y += self.baseline;
}
}
self.o.x += self.gp.adv;
self.adv = self.o.x - prev_o.x;
}
return b;
}
fn bool GlyphIterator.has_next(&self)
{
if (self.anchor == TOP_LEFT) {
return self.line_off < self.text.len;
}
if (self.line_idx >= self.lines.len()) {
return false;
}
LineInfo li = self.lines[self.line_idx];
if (self.line_idx == self.lines.len() - 1 && self.line_off >= li.len()) {
return false;
}
return true;
}
fn usz GlyphIterator.current_offset(&self)
{
if (self.anchor == TOP_LEFT) return self.line_off;
return self.lines[self.line_idx].start + self.line_off;
}
// layout a string inside a bounding box, following the given alignment (anchor).
<*
@param [&in] ctx
@param [in] text
*>
fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue, bool reflow = false)
{
if (text == "") return;
if (bounds.w <= 0 || bounds.h <= 0) return;
ctx.push_scissor(bounds, z_index)!;
Font* font = &ctx.font;
Id texture_id = font.id;
GlyphIterator gi;
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
while (gi.has_next()) {
Rect b = gi.next()!;
Rect uv = gi.gp.uv();
ctx.push_sprite(b, uv, texture_id, z_index, hue)!;
}
ctx.reset_scissor(z_index)!;
// ctx.dbg_rect(str_bounds.off(bounds.position()));
}
// ---------------------------------------------------------------------------------- //
// CURSOR AND MOUSE //
// ---------------------------------------------------------------------------------- //
fn Rect? Ctx.get_cursor_position(&ctx, String text, Rect bounds, Anchor anchor, usz cursor, bool reflow = false)
{
if (bounds.w <= 0 || bounds.h <= 0) return {};
Font* font = &ctx.font;
Id texture_id = font.id;
if (text == "") text = "\f";
GlyphIterator gi;
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
Rect cursor_rect;
cursor_rect.x = gi.o.x;
cursor_rect.y = gi.o.y;
cursor_rect.h = (short)font.line_height();
if (cursor == 0) return cursor_rect;
while (gi.has_next()) {
Rect b = gi.next()!;
if (gi.current_offset() == cursor) {
if (gi.cp == '\n') {
if (!gi.has_next()) {
cursor_rect.x = bounds.x + bounds.x_off(0, anchor);
cursor_rect.y = b.y + gi.line_height + gi.line_gap;
} else {
gi.next()!;
cursor_rect.x = gi.o.x - gi.gp.adv;
cursor_rect.y = gi.o.y;
}
} else {
// Use the updated origin position instead of glyph bounds
cursor_rect.x = gi.o.x;
cursor_rect.y = gi.o.y;
}
return cursor_rect;
}
}
return {};
}
<*
@param [&in] ctx
@param [in] text
*>
fn usz? Ctx.hit_test_string(&ctx, String text, Rect bounds, Anchor anchor, Point p, bool reflow = false)
{
if (text == "") return 0;
if (bounds.w <= 0 || bounds.h <= 0) return 0;
Font* font = &ctx.font;
GlyphIterator gi;
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
usz prev_offset = 0;
Point prev_o = gi.o;
while (gi.has_next()) {
Point o_before = gi.o;
usz offset_before = gi.current_offset();
Rect b = gi.next()!;
switch {
case gi.cp == '\n':
// Check if point is on this line before the newline
Rect line_rect = {
.x = prev_o.x,
.y = prev_o.y,
.w = (short)(o_before.x - prev_o.x),
.h = gi.line_height
};
if (p.in_rect(line_rect)) return offset_before;
prev_o = gi.o;
break;
case gi.cp == '\t':
// Check if point is in the tab space
Rect tab_rect = {
.x = o_before.x,
.y = o_before.y,
.w = gi.tab_width,
.h = gi.line_height
};
if (p.in_rect(tab_rect)) return offset_before;
break;
case ascii::is_cntrl((char)gi.cp):
break;
default:
// Create a hit test rect for this character
Rect hit_rect = {
.x = o_before.x,
.y = o_before.y,
.w = gi.gp.adv,
.h = gi.line_height
};
if (p.in_rect(hit_rect)) {
// Check if cursor should be before or after this character
// by checking which half of the character was clicked
short mid_x = o_before.x + gi.gp.adv / 2;
if (p.x < mid_x) {
return offset_before;
} else {
return gi.current_offset();
}
}
}
prev_offset = gi.current_offset();
}
// Point is after all text
return text.len;
}
// TODO: implement a function `layout_string_with_selection` to avoid iterating over the string twice
fn void? Ctx.draw_string_selection(&ctx, String text, Rect bounds, Anchor anchor, usz start, usz end, int z_index, Color hue, bool reflow = false)
{
if (text == "") return;
if (bounds.w <= 0 || bounds.h <= 0) return;
if (start > end) @swap(start, end);
// Ensure start < end
if (start > end) {
usz temp = start;
start = end;
end = temp;
}
ctx.push_scissor(bounds, z_index)!;
Font* font = &ctx.font;
GlyphIterator gi;
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
Rect sel_rect = { .h = gi.line_height }; // selection rect
isz sel_line = -1; // selection line
while (gi.has_next()) {
Rect b = gi.next()!;
usz off = gi.current_offset()-1;
isz line = gi.line_idx;
bool in_selection = start <= off && off <= end;
if (in_selection && line != sel_line) {
if (sel_line != -1) {
ctx.push_rect(sel_rect, z_index, &&{.bg = hue})!;
}
sel_rect = {.x = gi.o.x - gi.adv, .y = gi.o.y, .w = 0, .h = gi.line_height};
sel_line = line;
}
if (in_selection) {
sel_rect.w = gi.o.x - sel_rect.x;
if (gi.cp == '\n') sel_rect.w += gi.space_width;
}
if (off > end) break;
}
ctx.push_rect(sel_rect, z_index, &&{.bg = hue})!;
ctx.reset_scissor(z_index)!;
}
// ---------------------------------------------------------------------------------- //
// TEXT MEASUREMENT //
// ---------------------------------------------------------------------------------- //
const uint TAB_SIZE = 4;
struct TextSize {
Size width, height;
int area;
}
// Measeure the size of a string.
// width.min: as if each word is broken up by a new line
// width.max: the width of the string left as-is
// height.min: the height of the string left as-is
// height.max: the height of the string with each word broken up by a new line
<*
@param [&in] ctx
@param [in] text
*>
fn TextSize? Ctx.measure_string(&ctx, String text)
{
if (text == "") return (TextSize){};
Font* font = &ctx.font;
short baseline = (short)font.ascender;
short line_height = (short)font.line_height();
short line_gap = (short)font.linegap;
short space_width = font.get_glyph(' ').adv!;
short tab_width = space_width * TAB_SIZE;
isz off;
usz x;
TextSize ts;
short word_width;
short words = 1;
Rect bounds; // unaltered text bounds;
Point origin;
StringIterator it = text.iterator();
for (Codepoint cp; it.has_next();) {
cp = it.next()!;
Glyph* gp = font.get_glyph(cp)!;
// update the text bounds
switch {
case cp == '\n':
origin.x = 0;
origin.y += line_height + line_gap;
case cp == '\t':
origin.x += tab_width;
case ascii::is_cntrl((char)cp):
break;
default:
Rect b = gp.bounds().off(origin);
b.y += baseline;
bounds = containing_rect(bounds, b);
origin.x += gp.adv;
}
// update the word width
switch {
case ascii::is_space((char)cp):
if (word_width > ts.width.min) ts.width.min = word_width;
word_width = 0;
words++;
default:
//word_width += gp.w + gp.ox;
if (off < text.len) {
word_width += gp.adv;
} else {
word_width += gp.w + gp.ox;
}
}
}
// end of string is also end of word
if (word_width > ts.width.min) ts.width.min = word_width;
ts.width.max = bounds.w;
ts.height.min = bounds.h;
ts.height.max = words * line_height + line_gap * (words-1);
ts.area = bounds.w * bounds.h;
return ts;
}

461
src/style.c3 Normal file
View File

@ -0,0 +1,461 @@
module ugui;
import std::collections::map;
import std::core::mem::allocator;
import std::io;
// global style, similar to the css box model
struct Style { // css box model
Rect padding;
Rect border;
Rect margin;
Color bg; // background color
Color fg; // foreground color
Color primary; // primary color
Color secondary; // secondary color
Color accent; // accent color
ushort radius;
short size;
}
const Style DEFAULT_STYLE = {
.margin = {2, 2, 2, 2},
.border = {2, 2, 2, 2},
.padding = {1, 1, 1, 1},
.radius = 0,
.size = 16,
.bg = 0x282828ffu.@to_rgba(),
.fg = 0xfbf1c7ffu.@to_rgba(),
.primary = 0xcc241dffu.@to_rgba(),
.secondary = 0x458588ffu.@to_rgba(),
.accent = 0xfabd2fffu.@to_rgba(),
};
// style is stored in a hashmap, each style has an Id that can be generated by a string or whatever
alias StyleMap = map::HashMap{Id, Style};
// push or update a new style into the map
fn void StyleMap.register_style(&map, Style* style, Id id)
{
if (style == null) return;
map.set(id, *style);
}
// get a style from the map, if the style is not found then use a default style.
fn Style* StyleMap.get_style(&map, Id id)
{
Style*? s = map.get_ref(id);
if (catch s) {
// io::eprintfn("WARNING: style %x not found, using default style", id);
return &DEFAULT_STYLE;
}
return s;
}
fn int StyleMap.import_style_string(&map, String text)
{
Parser p;
p.lex.text = text;
int added;
while (p.parse_style() == true) {
added++;
// set the default style correctly
map.register_style(&p.style, p.style_id);
if (p.lex.peep_token().type == EOF) break;
}
return added;
}
fn int Ctx.import_style_from_string(&ctx, String text) => ctx.styles.import_style_string(text);
fn int Ctx.import_style_from_file(&ctx, String path)
{
char[] text;
usz size = file::get_size(path)!!;
text = mem::new_array(char, size);
file::load_buffer(path, text)!!;
defer mem::free(text);
int added = ctx.import_style_from_string((String)text);
return added;
}
/*
* Style can be serialized and deserialized with a subset of CSS
* <style name> {
* padding: left right top bottom;
* border: left right top bottom;
* margin: left right top bottom;
* radius: uint;
* size: uint;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* }
* The field "style name" will be hashed and the hash used as the id int the style map.
* Fields may be excluded, each excluded field is set to zero.
* The default unit is pixels, but millimeters is also available. The parser function accepts a scale
* factor that has to be obtained with the window manager functions.
*/
// TODO: implement <style name> : <another style> to easily inherit all properties
// of a previously defined style
module ugui::css;
import std::ascii;
import std::io;
// CSS parser module
enum TokenType {
INVALID,
IDENTIFIER,
RCURLY,
LCURLY,
SEMICOLON,
COLON,
NUMBER,
COLOR,
EOF,
}
enum Unit {
PIXELS,
MILLIMETERS
}
struct Token {
TokenType type;
usz line, col, off;
String text;
union {
struct {
float value;
Unit unit;
}
Color color;
}
}
fn short Token.to_px(&t, float mm_to_px)
{
if (t.type != NUMBER) {
unreachable("WFT you cannot convert to pixels a non-number");
}
if (t.unit == PIXELS) return (short)(t.value);
return (short)(t.value * mm_to_px);
}
// ---------------------------------------------------------------------------------- //
// LEXER //
// ---------------------------------------------------------------------------------- //
struct Lexer {
String text;
usz line, col, off;
}
macro char Lexer.peep(&lex) => lex.text[lex.off];
fn char Lexer.advance(&lex)
{
if (lex.off >= lex.text.len) return '\0';
char c = lex.text[lex.off];
if (c == '\n') {
lex.col = 0;
lex.line++;
} else {
lex.col++;
}
lex.off++;
return c;
}
fn Token Lexer.next_token(&lex)
{
Token t;
t.type = INVALID;
t.off = lex.off;
t.col = lex.col;
t.line = lex.line;
if (lex.off >= lex.text.len) {
return {.type = EOF};
}
// skip whitespace
while (ascii::is_space_m(lex.peep())) {
if (lex.advance() == 0) return {.type = EOF};
if (lex.off >= lex.text.len) return {.type = EOF};
}
t.off = lex.off;
switch (true) {
case ascii::is_punct_m(lex.peep()) && lex.peep() != '#': // punctuation
t.text = lex.text[lex.off:1];
if (lex.advance() == 0) { t.type = INVALID; break; }
switch (t.text[0]) {
case ':': t.type = COLON;
case ';': t.type = SEMICOLON;
case '{': t.type = LCURLY;
case '}': t.type = RCURLY;
default: t.type = INVALID;
}
case lex.peep() == '#': // color
t.type = COLOR;
if (lex.advance() == 0) { t.type = INVALID; break; }
usz hex_start = t.off+1;
while (ascii::is_alnum_m(lex.peep())) {
if (lex.advance() == 0) { t.type = INVALID; break; }
}
usz h_len = lex.off - hex_start;
if (h_len != 8 && h_len != 6) {
io::eprintfn("CSS lexing error at %d:%d: the only suppported color formats are #RRGGBBAA or #RRGGBB", t.line, t.col);
t.type = INVALID;
break;
}
char[10] hex_str = (char[])"0x";
hex_str[2:h_len] = lex.text[hex_start:h_len];
if (h_len == 6) hex_str[8..9] = "ff"[..];
uint? color_hex = ((String)hex_str[..]).to_uint();
if (catch color_hex) {
t.type = INVALID;
break;
}
t.color = color_hex.to_rgba();
case ascii::is_alpha_m(lex.peep()): // identifier
t.type = IDENTIFIER;
while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '-' || lex.peep() == '_') {
if (lex.advance() == 0) { t.type = INVALID; break; }
}
t.text = lex.text[t.off..lex.off-1];
case ascii::is_digit_m(lex.peep()): // number
t.type = NUMBER;
t.unit = PIXELS;
// find the end of the number
usz end;
while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '+' || lex.peep() == '-' || lex.peep() == '.') {
if (lex.advance() == 0) { t.type = INVALID; break; }
}
end = lex.off;
if (end - t.off > 2) {
if (lex.text[end-2:2] == "px") {
t.unit = PIXELS;
end -= 2;
} else if (lex.text[end-2:2] == "mm") {
t.unit = MILLIMETERS;
end -= 2;
} else if (lex.text[end-2:2] == "pt" || lex.text[end-2:2] == "em") {
io::eprintn("units 'em' or 'pt' are not supported at the moment");
t.type = INVALID;
break;
}
}
String number_str = lex.text[t.off..end-1];
float? value = number_str.to_float();
if (catch value) { t.type = INVALID; break; }
t.value = value;
t.text = lex.text[t.off..lex.off-1];
}
if (t.type == INVALID) {
io::eprintfn("CSS Lexing ERROR at %d:%d: '%s' is not a valid token", t.line, t.col, lex.text[t.off..lex.off]);
}
return t;
}
fn Token Lexer.peep_token(&lex)
{
Lexer start_state = *lex;
Token t;
t = lex.next_token();
*lex = start_state;
return t;
}
// ---------------------------------------------------------------------------------- //
// PARSER //
// ---------------------------------------------------------------------------------- //
struct Parser {
Lexer lex;
Style style;
Id style_id;
float mm_to_px;
}
macro bool Parser.expect(&p, Token* t, TokenType type)
{
*t = p.lex.next_token();
if (t.type == type) return true;
io::eprintfn("CSS parsing error at %d:%d: expected %s but got %s", t.line, t.col, type, t.type);
return false;
}
// style := ( IDENTIFIER "{" property_list "}" )
// property_list := property property_list
fn bool Parser.parse_style(&p)
{
Token t;
p.style = {};
p.style_id = 0;
// style name
if (p.expect(&t, IDENTIFIER) == false) return false;
p.style_id = t.text.hash();
// style body
if (p.expect(&t, LCURLY) == false) return false;
while (true) {
if (p.parse_property() == false) return false;
t = p.lex.peep_token();
if (t.type != IDENTIFIER) break;
}
if (p.expect(&t, RCURLY) == false) return false;
return true;
}
// property := ( number_property | color_property | size_property ) ";"
// number_property := ( "radius" | "size" ) "=" number
// color_property := ( "bg" | "fg" | "primary" | "secondary" | "accent" ) "=" color
// size_property := ( "padding" | "border" | "margin" ) "=" size
fn bool Parser.parse_property(&p)
{
Token t, prop;
if (p.expect(&prop, IDENTIFIER) == false) return false;
if (p.expect(&t, COLON) == false) return false;
switch (prop.text) {
case "padding":
Rect padding;
if (p.parse_size(&padding) == false) return false;
p.style.padding = padding;
case "border":
Rect border;
if (p.parse_size(&border) == false) return false;
p.style.border = border;
case "margin":
Rect margin;
if (p.parse_size(&margin) == false) return false;
p.style.margin = margin;
case "bg":
Color bg;
if (p.parse_color(&bg) == false) return false;
p.style.bg = bg;
case "fg":
Color fg;
if (p.parse_color(&fg) == false) return false;
p.style.fg = fg;
case "primary":
Color primary;
if (p.parse_color(&primary) == false) return false;
p.style.primary = primary;
case "secondary":
Color secondary;
if (p.parse_color(&secondary) == false) return false;
p.style.secondary = secondary;
case "accent":
Color accent;
if (p.parse_color(&accent) == false) return false;
p.style.accent = accent;
case "radius":
short r;
if (p.parse_number(&r) == false) return false;
if (r < 0) {
io::eprintfn("CSS parsing error at %d:%d: 'radius' must be a positive number, got %d", t.line, t.col, r);
return false;
}
p.style.radius = (ushort)r;
case "size":
short s;
if (p.parse_number(&s) == false) return false;
if (s < 0) {
io::eprintfn("CSS parsing error at %d:%d: 'size' must be a positive number, got %d", t.line, t.col, s);
return false;
}
p.style.size = (ushort)s;
default:
io::eprintfn("CSS parsing error at %d:%d: '%s' is not a valid property", prop.line, prop.col, prop.text);
return false;
}
if (p.expect(&t, SEMICOLON) == false) return false;
return true;
}
// number := NUMBER
fn bool Parser.parse_number(&p, short* n)
{
Token t;
if (p.expect(&t, NUMBER) == false) return false;
*n = t.to_px(p.mm_to_px);
return true;
}
// color := COLOR
// FIXME: since '#' is punctuation this cannot be done in parsing but it has to be done in lexing
fn bool Parser.parse_color(&p, Color* c)
{
Token t;
if (p.expect(&t, COLOR) == false) return false;
*c = t.color;
return true;
}
// size := number | number_list
// number_list := number number number number
fn bool Parser.parse_size(&p, Rect* r)
{
short x;
Token t;
if (p.parse_number(&x) == false) return false;
t = p.lex.peep_token();
if (t.type == NUMBER) {
// we got another number so we expect three more
r.x = x;
if (p.parse_number(&x) == false) return false;
r.y = x;
if (p.parse_number(&x) == false) return false;
r.w = x;
if (p.parse_number(&x) == false) return false;
r.h = x;
return true;
} else if (t.type == SEMICOLON) {
// just one number, all dimensions are the same
r.x = r.y = r.w = r.h = x;
return true;
}
return false;
}

288
src/textedit.c3 Normal file
View File

@ -0,0 +1,288 @@
module ugui;
import ugui::textedit::te;
struct TextEdit {
char[] buffer;
usz chars;
usz cursor;
isz sel_len;
}
fn String TextEdit.to_string(&te) => (String)te.buffer[:te.chars];
fn String TextEdit.until_cursor(&te) => (String)te.buffer[..te.cursor];
fn String TextEdit.from_cursor(&te) => (String)te.buffer[te.cursor..te.chars];
fn String TextEdit.until(&te, usz off) => (String)te.buffer[..min(te.chars, off)];
fn String TextEdit.from(&te, usz off) => (String)te.buffer[off..te.chars];
// implement text editing operations on the buffer
// returns true if the buffer is full
fn bool Ctx.text_edit(&ctx, TextEdit* te)
{
String in = ctx.get_keys();
ModKeys mod = ctx.get_mod();
te.insert_utf8(in);
bool select = !!(mod & KMOD_SHIFT);
bool ctrl = !!(mod & KMOD_CTRL);
// handle backspace and delete
if (mod.bkspc) {
if (ctrl) {
te.remove_word(false);
} else {
te.remove_character(false);
}
}
if (mod.del) {
if (ctrl) {
te.remove_word(true);
} else {
te.remove_character(true);
}
}
// handle arrow keys
if (mod.left) {
if (ctrl) {
te.move_cursor_word(false, select);
} else {
te.move_cursor(false, select);
}
}
if (mod.right) {
if (ctrl) {
te.move_cursor_word(true, select);
} else {
te.move_cursor(true, select);
}
}
if (mod.home) {
te.set_cursor(te.search_lines(false).first, select);
}
if (mod.end) {
te.set_cursor(te.search_lines(true).first, select);
}
// TODO: up, down
return te.chars < te.buffer.len;
}
module ugui::textedit::te;
import std::core::string;
import std::ascii;
import std::collections::pair;
alias OffPair = pair::Pair{usz, usz};
// returns the offset of the next codepoint in the buffer from the cursor
fn usz TextEdit.next_char_off(&te)
{
usz len = min(te.chars - te.cursor, 4);
if (len == 0) return len;
conv::utf8_to_char32(&te.buffer[te.cursor], &len)!!;
return len;
}
// returns the offset of the previous codepoint in the buffer from the cursor
fn usz TextEdit.prev_char_off(&te)
{
if (te.cursor == 0) return 0;
String b = (String)te.buffer[..te.cursor];
usz len;
foreach_r (off, c: b) {
if (c & 0xC0 == 0x80) continue;
len = b.len - off;
break;
}
// verify the utf8 character
conv::utf8_to_char32(&b[te.cursor - len], &len)!!;
return len;
}
// moves the cursor forwards or backwards by one codepoint without exiting the bounds, if select is
// true also change the selection width
fn void TextEdit.move_cursor(&te, bool forward, bool select)
{
// in selection but trying to move without selecting, snap the cursor and reset selection
if (te.sel_len != 0 && !select) {
if (te.sel_len > 0 && forward) {
// selection is in front of the cursor and trying to move right, snap the cursor to the
// end of selection
te.cursor += te.sel_len;
} else if (te.sel_len < 0 && !forward) {
// selection is behind the cursor and trying to move left, snap the cursor to the start
// of selection
te.cursor += te.sel_len;
}
te.sel_len = 0;
} else {
isz off = forward ? te.next_char_off() : -te.prev_char_off();
if (te.cursor + off < 0 || te.cursor + off > te.chars) return;
te.cursor += off;
// if trying to select increment selection width
if (select) {
te.sel_len -= off;
}
}
}
// set the cursor to the exact offset provided, the selection flags controls wether the selection has
// expanded or rese
fn void TextEdit.set_cursor(&te, usz cur, bool select)
{
if (!select) te.sel_len = 0;
if (cur == te.cursor) return;
usz prev_cur = te.cursor;
te.cursor = cur;
if (select) {
te.sel_len += prev_cur - cur;
}
}
fn void TextEdit.move_cursor_word(&te, bool forward, bool select)
{
// moving out of selection, snap to the end
if (!select && te.sel_len != 0) {
te.move_cursor(forward, select);
return;
}
usz prev_cur = te.cursor;
while (te.cursor <= te.chars) {
char c;
if (forward) {
if (te.cursor == te.chars) break;
c = te.buffer[te.cursor];
} else {
if (te.cursor == 0) break;
c = te.buffer[te.cursor-1];
}
if (ascii::is_space(c) || ascii::is_punct(c)) break;
te.move_cursor(forward, select);
}
// move at least one character
if (prev_cur == te.cursor) te.move_cursor(forward, select);
}
// get the offset of the current line start and the previous line start from the cursor
fn OffPair TextEdit.search_lines(&te, bool forward)
{
// look for the current line start
OffPair p;
if (forward) {
// if searching forwards look for the end of the current line and the end of the next line
p.first = te.cursor + (te.from_cursor().index_of_char('\n') ?? (te.chars - te.cursor));
if (p.first < te.chars) {
p.second = p.first + 1 + (te.from(p.first + 1).index_of_char('\n') ?? (te.chars - p.first - 1));
} else {
p.second = te.chars;
}
} else {
// if searching backwards, look for the start of the current and previous line
if (te.cursor > 0) {
p.first = te.until(te.cursor-1).rindex_of_char('\n') ?? 0;
if (p.first != 0 && te.chars) p.first++;
}
p.second = te.until(p.first).rindex_of_char('\n') ?? 0;
}
return p;
}
fn void TextEdit.delete_selection(&te)
{
if (te.sel_len == 0) return;
usz start = te.sel_len > 0 ? te.cursor : te.cursor + te.sel_len;
usz end = te.sel_len > 0 ? te.cursor + te.sel_len : te.cursor;
usz len = te.chars - end;
te.buffer[start:len] = te.buffer[end:len];
te.cursor -= te.sel_len < 0 ? -te.sel_len : 0;
te.chars -= te.sel_len < 0 ? -te.sel_len : te.sel_len;
te.sel_len = 0;
}
// removes the character before or after the cursor
fn void TextEdit.remove_character(&te, bool forward)
{
// if there is a selection active then delete that selection
if (te.sel_len) {
te.delete_selection();
return;
}
if (te.chars == 0) return;
if (forward) {
usz rem = te.chars - te.cursor;
if (rem > 0) {
usz len = te.next_char_off();
te.buffer[te.cursor:rem-len] = te.buffer[te.cursor+len:rem-len];
te.chars -= len;
}
} else {
if (te.cursor > 0) {
usz len = te.prev_char_off();
te.buffer[te.cursor-len:te.chars-te.cursor] = te.buffer[te.cursor:te.chars-te.cursor];
te.chars -= len;
te.cursor -= len;
}
}
}
// remove the word before or after the cursor up until the next punctuation or space
fn void TextEdit.remove_word(&te, bool forward)
{
// if there is a selection active then delete that selection
if (te.sel_len) {
te.delete_selection();
return;
}
usz prev_cur = te.cursor;
while (te.chars > 0) {
char c;
if (forward) {
if (te.cursor == te.chars) break;
c = te.buffer[te.cursor];
} else {
if (te.cursor == 0) break;
c = te.buffer[te.cursor-1];
}
if (ascii::is_space(c) || ascii::is_punct(c)) break;
te.remove_character(forward);
}
// delete at least one character
if (prev_cur == te.cursor) te.remove_character(forward);
}
// insert a character at the cursor and update the cursor
fn void TextEdit.insert_character(&te, uint cp)
{
char[4] b;
usz len = conv::char32_to_utf8(cp, b[..])!!;
te.insert_utf8((String)b[:len]);
}
fn void TextEdit.insert_utf8(&te, String s)
{
if (s.len == 0) return;
if (s.len + te.chars > te.buffer.len) return;
te.delete_selection();
te.buffer[te.cursor+s.len : te.chars-te.cursor] = te.buffer[te.cursor : te.chars-te.cursor];
te.buffer[te.cursor : s.len] = s[..];
te.chars += s.len;
te.cursor += s.len;
}

226
src/widgets/button.c3 Normal file
View File

@ -0,0 +1,226 @@
module ugui;
import std::io;
// button element
struct ElemButton {
int filler;
}
macro Ctx.button(&ctx, String label = "", String icon = "", ...)
=> ctx.button_id(@compute_id($vasplat), label, icon);
fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_BUTTON)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("button"));
Sprite* sprite = icon != "" ? ctx.sprite_atlas.get(icon)! : &&(Sprite){};
Rect icon_size = sprite.rect();
ushort min_size = style.size;
ushort half_lh = (ushort)(ctx.font.line_height() / 2);
ushort inner_pad = label != "" && icon != "" ? half_lh : 0;
/*
* +--------------------------------------+
* | +--------+ |
* | | | +-----------------+ |
* | | icon | | label | |
* | | | +-----------------+ |
* | +--------+<->| |
* +-------------^------------------------+
* |inner_pad
*/
Point content_size = {
.x = icon_size.w + inner_pad, // text sizing is handled differently
.y = icon_size.h + inner_pad,
};
elem.layout.w = @fit(min_size);
elem.layout.h = @fit(min_size);
elem.layout.children.w = @exact(content_size.x);
elem.layout.children.h = @exact(content_size.y);
elem.layout.text = ctx.measure_string(label)!;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
//elem.events = ctx.get_elem_events(elem);
// if (ctx.input.z_index == elem.z_index) println("true ", elem.z_index);
Rect content_bounds = elem.content_bounds();
Rect icon_bounds = {
.x = content_bounds.x,
.y = content_bounds.y,
.w = icon_size.w,
.h = icon_size.h
};
icon_bounds = icon_size.center_to(icon_bounds);
Rect text_bounds = {
.x = content_bounds.x + icon_bounds.w + inner_pad,
.y = content_bounds.y,
.w = content_bounds.w - icon_bounds.w - inner_pad,
.h = content_bounds.h,
};
//text_bounds = text_size.center_to(text_bounds);
bool is_active = elem.events.has_focus || elem.events.mouse_hover;
Style s = *style;
if (is_active) {
s.secondary = s.primary;
s.bg = s.accent;
}
ctx.push_rect(elem.bounds.pad(style.margin), parent.z_index, &s)!;
if (icon != "") {
ctx.push_sprite(icon_bounds, sprite.uv(), ctx.sprite_atlas.id, parent.z_index, type: sprite.type)!;
}
if (label != "") {
ctx.layout_string(label, text_bounds, CENTER, parent.z_index, style.fg)!;
}
return elem.events;
}
macro Ctx.checkbox(&ctx, String desc, bool* active, String tick_sprite = "", ...)
=> ctx.checkbox_id(@compute_id($vasplat), desc, active, tick_sprite);
fn void? Ctx.checkbox_id(&ctx, Id id, String description, bool* active, String tick_sprite)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_BUTTON)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("checkbox"));
short inner_pad = description != "" ? style.size/2 : 0;
/*
* |< >| style.size/2
* +---------------------|---|-----------+
* | | .-----. ---|--
* | +-----------------+ ' ### ' | ^
* | | description | | ##### | | style.size
* | +-----------------+ . ### . | v
* | '-----' ---|--
* +-------------------------|-------|---+
* |<----->| style.size
*/
elem.layout.w = @fit(style.size);
elem.layout.h = @fit(style.size);
elem.layout.children.w = @exact(style.size + inner_pad);
elem.layout.children.h = @exact(style.size);
elem.layout.text = ctx.measure_string(description)!;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
//elem.events = ctx.get_elem_events(elem);
if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
Rect content_bounds = elem.bounds.pad(elem.layout.content_offset);
Rect text_bounds = {
.x = content_bounds.x,
.y = content_bounds.y,
.w = content_bounds.w - inner_pad - style.size,
.h = content_bounds.h
};
Rect check_bounds = {
.x = content_bounds.x + text_bounds.w + inner_pad,
.y = content_bounds.y + (content_bounds.h - style.size)/2,
.w = style.size,
.h = style.size,
};
Style s;
s.bg = style.bg;
s.secondary = style.secondary;
s.border = style.border;
s.radius = style.radius;
ctx.layout_string(description, text_bounds, CENTER, parent.z_index, style.fg)!;
if (tick_sprite != "") {
ctx.push_rect(check_bounds, parent.z_index, &s)!;
if (*active) {
Sprite* sprite = ctx.sprite_atlas.get(tick_sprite)!;
Id tex_id = ctx.sprite_atlas.id;
ctx.push_sprite(check_bounds, sprite.uv(), tex_id, parent.z_index, type: sprite.type)!;
}
} else {
if (*active) {
s.bg = style.primary;
ctx.push_rect(check_bounds, parent.z_index, &s)!;
} else {
ctx.push_rect(check_bounds, parent.z_index, &s)!;
}
}
}
macro Ctx.toggle(&ctx, String desc, bool* active)
=> ctx.toggle_id(@compute_id($vasplat), desc, active);
fn void? Ctx.toggle_id(&ctx, Id id, String description, bool* active)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_BUTTON)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("toggle"));
short inner_pad = description != "" ? style.size/2 : 0;
/*
* |< >| style.size/2
* +---------------------|---|-----------------+
* | | .-----------. ---|--
* | +-----------------+ ' ##### ' | ^
* | | description | | ##### | | style.size
* | +-----------------+ . ##### . | v
* | '-----------' ---|--
* +-------------------------|-------------|---+
* |<----->| style.size*2
*/
elem.layout.w = @fit(style.size*2);
elem.layout.h = @fit(style.size);
elem.layout.children.w = @exact(style.size*2 + inner_pad);
elem.layout.children.h = @exact(style.size);
elem.layout.text = ctx.measure_string(description)!;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
//elem.events = ctx.get_elem_events(elem);
if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
Rect content_bounds = elem.bounds.pad(elem.layout.content_offset);
Rect text_bounds = {
.x = content_bounds.x,
.y = content_bounds.y,
.w = content_bounds.w - inner_pad - style.size*2,
.h = content_bounds.h
};
Rect toggle_bounds = {
.x = content_bounds.x + text_bounds.w + inner_pad,
.y = content_bounds.y + (content_bounds.h - style.size)/2,
.w = style.size*2,
.h = style.size,
};
Rect toggle = {
.x = toggle_bounds.x + (*active ? style.size : 0),
.y = toggle_bounds.y,
.w = style.size,
.h = style.size
};
Style s;
s.bg = style.bg;
s.secondary = style.secondary;
s.border = style.border;
s.radius = style.radius;
ctx.layout_string(description, text_bounds, CENTER, parent.z_index, style.fg)!;
ctx.push_rect(toggle_bounds, parent.z_index, &s)!;
s.bg = style.primary;
s.border = {};
ctx.push_rect(toggle.pad(style.border), parent.z_index, &s)!;
}

252
src/widgets/div.c3 Normal file
View File

@ -0,0 +1,252 @@
module ugui;
import std::io;
import std::math;
// div element
struct ElemDiv {
struct scroll_x {
bool enabled;
bool on;
float value;
}
struct scroll_y {
bool enabled;
bool on;
float value;
}
}
macro Ctx.@center(&ctx, LayoutDirection dir = ROW, ...; @body())
{
return ctx.@div(@grow(), @grow(), dir, CENTER) {
@body();
}!;
}
macro Ctx.@row(&ctx, Anchor anchor = TOP_LEFT, ...; @body())
{
return ctx.@div(@fit(), @fit(), ROW, anchor: anchor) {
@body();
}!;
}
macro Ctx.@column(&ctx, Anchor anchor = TOP_LEFT, ...; @body())
{
return ctx.@div(@fit(), @fit(), COLUMN, anchor: anchor) {
@body();
}!;
}
// useful macro to start and end a div, capturing the trailing block
macro Ctx.@div(&ctx,
Size width = @grow, Size height = @grow,
LayoutDirection dir = ROW,
Anchor anchor = TOP_LEFT,
bool absolute = false, Point off = {},
bool scroll_x = false, bool scroll_y = false,
...;
@body()
)
{
ctx.div_begin(width, height, dir, anchor, absolute, off, scroll_x, scroll_y, $vasplat)!;
@body();
return ctx.div_end()!;
}
macro Ctx.div_begin(&ctx,
Size width = @grow(), Size height = @grow(),
LayoutDirection dir = ROW,
Anchor anchor = TOP_LEFT,
bool absolute = false, Point off = {},
bool scroll_x = false, bool scroll_y = false,
...
)
{
return ctx.div_begin_id(@compute_id($vasplat), width, height, dir, anchor, absolute, off, scroll_x, scroll_y);
}
fn void? Ctx.div_begin_id(&ctx,
Id id,
Size width, Size height,
LayoutDirection dir,
Anchor anchor,
bool absolute, Point off,
bool scroll_x, bool scroll_y
)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_DIV)!.unpack(&elem, &parent);
ctx.active_div = elem.tree_idx;
Style* style = ctx.styles.get_style(@str_hash("div"));
elem.div.scroll_x.enabled = scroll_x;
elem.div.scroll_y.enabled = scroll_y;
// update layout with correct info
elem.layout = {
.w = width,
.h = height,
.dir = dir,
.anchor = anchor,
.content_offset = style.margin + style.border + style.padding,
.absolute = absolute,
};
if (absolute) {
elem.layout.origin.x = off.x;
elem.layout.origin.y = off.y;
}
ctx.push_rect(elem.bounds.pad(style.margin), elem.z_index, style)!;
// update the ctx scissor, it HAS to be after drawing the background
ctx.div_scissor = elem.bounds.pad(elem.layout.content_offset).max({0,0,0,0});
ctx.push_scissor(ctx.div_scissor, elem.z_index)!;
//elem.events = ctx.get_elem_events(elem);
// TODO: check active
// TODO: check resizeable
}
fn Id? Ctx.div_end(&ctx)
{
Elem* elem = ctx.get_active_div()!;
Style* style = ctx.styles.get_style(@str_hash("div"));
Rect bounds = elem.bounds.pad(style.margin + style.border);
// set the scrollbar flag, is used in layout
Point cbc = elem.children_bounds.bottom_right();
Point bc = elem.bounds.bottom_right();
// horizontal overflow
elem.div.scroll_x.on = cbc.x > bc.x && elem.div.scroll_x.enabled;
// vertical overflow
elem.div.scroll_y.on = cbc.y > bc.y && elem.div.scroll_y.enabled;
Id hsid_raw = @str_hash("div_scrollbar_horizontal");
Id vsid_raw = @str_hash("div_scrollbar_vertical");
Id hsid_real = ctx.gen_id(hsid_raw)!;
Id vsid_real = ctx.gen_id(vsid_raw)!;
if (elem.div.scroll_y.on) {
if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id && !(ctx.get_mod() & KMOD_SHIFT)) {
elem.div.scroll_y.value += ctx.input.mouse.scroll.y * 0.07f;
elem.div.scroll_y.value = math::clamp(elem.div.scroll_y.value, 0.0f, 1.0f);
}
ctx.scrollbar(vsid_raw, &elem.div.scroll_y.value, max((float)bc.y / cbc.y, (float)0.15))!;
elem.layout.scroll_offset.y = (short)(elem.div.scroll_y.value*(float)(elem.children_bounds.h-elem.bounds.h));
} else {
elem.div.scroll_y.value = 0;
}
if (elem.div.scroll_x.on) {
if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
if (ctx.get_mod() & KMOD_SHIFT) { // horizontal scroll with shift
elem.div.scroll_x.value += ctx.input.mouse.scroll.y * 0.07f;
elem.div.scroll_x.value = math::clamp(elem.div.scroll_x.value, 0.0f, 1.0f);
} else {
elem.div.scroll_x.value += ctx.input.mouse.scroll.x * 0.07f;
elem.div.scroll_x.value = math::clamp(elem.div.scroll_x.value, 0.0f, 1.0f);
}
}
ctx.scrollbar(hsid_raw, &elem.div.scroll_x.value, max((float)bc.x / cbc.x, (float)0.15), false)!;
elem.layout.scroll_offset.x = (short)(elem.div.scroll_x.value*(float)(elem.children_bounds.w-elem.bounds.w));
} else {
elem.div.scroll_x.value = 0;
}
// the active_div returns to the parent of the current one
ctx.active_div = ctx.tree.parentof(ctx.active_div)!;
Elem* parent = ctx.get_parent()!;
ctx.div_scissor = parent.bounds.pad(parent.layout.content_offset).max({0,0,0,0});
ctx.reset_scissor(elem.z_index)!;
update_parent_size(elem, parent);
return elem.id;
}
<* @param[&inout] state *>
macro Ctx.@popup(&ctx, bool* state,
Point pos,
Size width, Size height,
LayoutDirection dir = ROW, Anchor anchor = TOP_LEFT,
bool scroll_x = false, bool scroll_y = false,
...; @body())
{
if (*state) {
*state = ctx.popup_begin(pos, width, height, dir, anchor, scroll_x, scroll_y)!;
@body();
ctx.div_end()!;
}
}
macro bool? Ctx.popup_begin(&ctx, Point pos,
Size width, Size height,
LayoutDirection dir = ROW, Anchor anchor = TOP_LEFT,
bool scroll_x = false, bool scroll_y = false,
...
)
=> ctx.popup_begin_id(@compute_id($vasplat), pos, width, height, dir, anchor, scroll_x, scroll_y);
fn bool? Ctx.popup_begin_id(&ctx,
Id id,
Point pos,
Size width, Size height,
LayoutDirection dir, Anchor anchor,
bool scroll_x, bool scroll_y
)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_DIV)!.unpack(&elem, &parent);
ctx.active_div = elem.tree_idx;
Style* style = ctx.styles.get_style(@str_hash("popup"));
elem.div.scroll_x.enabled = scroll_x;
elem.div.scroll_y.enabled = scroll_y;
elem.z_index++;
// update layout with correct info
elem.layout = {
.w = width,
.h = height,
.dir = dir,
.anchor = anchor,
.content_offset = style.margin + style.border + style.padding,
.absolute = true,
.origin.x = pos.x - parent.bounds.x,
.origin.y = pos.y - parent.bounds.y,
};
// if the position is not the one expected then request a frame-skip to not flash appear in the
// wrong position for one frame
if (elem.bounds.position() != pos) {
ctx.skip_frame = true;
return true;
}
ctx.push_rect(elem.bounds.pad(style.margin), elem.z_index, style)!;
// update the ctx scissor, it HAS to be after drawing the background
ctx.div_scissor = elem.bounds.pad(elem.layout.content_offset).max({0,0,0,0});
ctx.push_scissor(ctx.div_scissor, elem.z_index)!;
//elem.events = ctx.get_elem_events(elem);
// check close condition, mouse release anywhere outside the div bounds
if ((ctx.mouse_released() & BTN_ANY) && ctx.input.mouse.pos.outside(elem.bounds)) {
return false;
}
// TODO: check active
// TODO: check resizeable
return true;
}

56
src/widgets/separator.c3 Normal file
View File

@ -0,0 +1,56 @@
module ugui;
macro Ctx.separator(&ctx, Size width, Size height, ...)
=> ctx.separator_id(@compute_id($vasplat), width, height);
fn void? Ctx.separator_id(&ctx, Id id, Size width, Size height)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_NONE)!.unpack(&elem, &parent);
elem.layout.w = width;
elem.layout.h = height;
update_parent_size(elem, parent);
}
macro Ctx.hor_line(&ctx, ...)
=> ctx.hor_line_id(@compute_id($vasplat));
fn void? Ctx.hor_line_id(&ctx, Id id)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_NONE)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("separator"));
elem.layout.w = @grow();
elem.layout.h = @exact(style.size);
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
Rect r = elem.bounds.pad(elem.layout.content_offset);
ctx.push_rect(r, elem.z_index, style)!;
}
macro Ctx.ver_line(&ctx, ...)
=> ctx.ver_line_id(@compute_id($vasplat));
fn void? Ctx.ver_line_id(&ctx, Id id)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_NONE)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("separator"));
elem.layout.w = @exact(style.size);
elem.layout.h = @grow();
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
Rect r = elem.bounds.pad(elem.layout.content_offset);
ctx.push_rect(r, elem.z_index, style)!;
}

185
src/widgets/slider.c3 Normal file
View File

@ -0,0 +1,185 @@
module ugui;
import std::io;
import std::math;
// slider element
struct ElemSlider {
Rect handle;
}
/* handle
* +----+-----+---------------------+
* | |#####| |
* +----+-----+---------------------+
*/
macro Ctx.slider_hor(&ctx, Size w, Size h, float* value, float hpercent = 0.25, ...)
=> ctx.slider_hor_id(@compute_id($vasplat), w, h, value, hpercent);
<*
@require value != null
*>
fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Size w, Size h, float* value, float hpercent = 0.25)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_SLIDER)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("slider"));
elem.layout.w = w;
elem.layout.h = h;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
Rect bg_bounds = elem.bounds.pad(style.margin);
Rect content_bounds = elem.bounds.pad(style.margin + style.border + style.padding);
// handle width
short hw = (short)(content_bounds.w * hpercent);
Rect handle = {
.x = calc_slider(content_bounds.x, content_bounds.w-hw, *value),
.y = content_bounds.y,
.w = hw,
.h = content_bounds.h,
};
elem.slider.handle = handle;
Point m = ctx.input.mouse.pos;
//elem.events = ctx.get_elem_events(elem);
if (elem.events.has_focus && ctx.is_mouse_down(BTN_LEFT)) {
*value = calc_value(content_bounds.x, m.x, content_bounds.w, hw);
elem.slider.handle.x = calc_slider(content_bounds.x, content_bounds.w-hw, *value);
elem.events.update = true;
}
// Draw the slider background and handle
Style s = *style;
Rect padding = s.padding;
s.padding = {};
ctx.push_rect(bg_bounds, parent.z_index, &s)!;
s.bg = s.primary;
s.padding = padding;
s.border = {};
ctx.push_rect(elem.slider.handle, parent.z_index, &s)!;
return elem.events;
}
/*
* +--+
* | |
* | |
* +--+
* |##| handle
* |##|
* +--+
* | |
* | |
* +--+
*/
macro Ctx.slider_ver(&ctx, Size w, Size h, float* value, float hpercent = 0.25, ...)
=> ctx.slider_ver_id(@compute_id($vasplat), w, h, value, hpercent);
fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Size w, Size h, float* value, float hpercent = 0.25)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_SLIDER)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("slider"));
elem.layout.w = w;
elem.layout.h = h;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
// 2. Layout
Rect bg_bounds = elem.bounds.pad(style.margin);
Rect content_bounds = elem.bounds.pad(style.margin + style.border + style.padding);
// handle height
short hh = (short)(content_bounds.h * hpercent);
Rect handle = {
.x = content_bounds.x,
.y = calc_slider(content_bounds.y, content_bounds.h-hh, *value),
.w = content_bounds.w,
.h = hh,
};
elem.slider.handle = handle;
Point m = ctx.input.mouse.pos;
//elem.events = ctx.get_elem_events(elem);
if (elem.events.has_focus && ctx.is_mouse_down(BTN_LEFT)) {
*value = calc_value(content_bounds.y, m.y, content_bounds.h, hh);
elem.slider.handle.y = calc_slider(content_bounds.y, content_bounds.h-hh, *value);
elem.events.update = true;
}
// Draw the slider background and handle
Style s = *style;
Rect padding = s.padding;
s.padding = {};
ctx.push_rect(bg_bounds, parent.z_index, &s)!;
s.bg = s.primary;
s.padding = padding;
s.border = {};
ctx.push_rect(elem.slider.handle, parent.z_index, &s)!;
return elem.events;
}
fn void? Ctx.scrollbar(&ctx, Id id, float *value, float handle_percent, bool vertical = true)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_SLIDER)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("scrollbar"));
Rect pb = parent.bounds.pad(parent.layout.content_offset);
if (vertical) {
elem.layout.w = @exact(style.size);
elem.layout.h = @grow();
elem.layout.origin.x = pb.w - style.size;
elem.layout.origin.y = 0;
} else {
elem.layout.w = @grow();
elem.layout.h = @exact(style.size);
elem.layout.origin.x = 0;
elem.layout.origin.y = pb.h - style.size;
}
elem.layout.content_offset = style.margin + style.border + style.padding;
elem.layout.absolute = true;
update_parent_size(elem, parent);
Rect content_bounds = elem.bounds.pad(elem.layout.content_offset);
short o = vertical ? content_bounds.y : content_bounds.x;
short m = vertical ? ctx.input.mouse.pos.y : ctx.input.mouse.pos.x;
short s = vertical ? content_bounds.h : content_bounds.w;
short h = (short)((float)s * handle_percent);
if (elem.events.has_focus && ctx.is_mouse_down(BTN_LEFT)) {
*value = calc_value(o, m, s, h);
elem.events.update = true;
}
short handle_pos = calc_slider(o, s-h, *value);
elem.slider.handle = {
.x = vertical ? content_bounds.x : handle_pos,
.y = vertical ? handle_pos : content_bounds.y,
.w = vertical ? content_bounds.w : h,
.h = vertical ? h : content_bounds.h,
};
Rect bg_bounds = elem.bounds.pad(style.margin);
ctx.push_rect(bg_bounds, parent.z_index, style)!;
ctx.push_rect(elem.slider.handle, parent.z_index, &&(Style){.bg = style.primary, .radius = style.radius})!;
}
macro short calc_slider(short off, short dim, float value) => (short)off + (short)(dim * value);
macro float calc_value(short off, short mouse, short dim, short slider)
=> math::clamp((float)(mouse-off-slider/2)/(float)(dim-slider), 0.0f, 1.0f);

41
src/widgets/sprite.c3 Normal file
View File

@ -0,0 +1,41 @@
module ugui;
struct ElemSprite {
Id id;
}
macro Ctx.sprite(&ctx, String name, short size = 0, ...)
=> ctx.sprite_id(@compute_id($vasplat), name, size);
fn void? Ctx.sprite_id(&ctx, Id id, String name, short size = 0)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_SPRITE)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("sprite"));
Sprite* sprite = ctx.sprite_atlas.get(name)!;
elem.sprite.id = ctx.get_sprite_atlas_id(name);
// scale the sprite so that the biggest dimension becomes "size"
short width = sprite.w;
short height = sprite.h;
if (size > 0) {
if (sprite.w >= sprite.h) {
height = (short)(size * (float)height/width);
width = size;
} else {
width = (short)(size * (float)width/height);
height = size;
}
}
elem.layout.w = elem.layout.children.w = @exact(width);
elem.layout.h = elem.layout.children.h = @exact(height);
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
Id tex_id = ctx.sprite_atlas.id;
Rect content_bounds = elem.bounds.pad(elem.layout.content_offset);
return ctx.push_sprite(content_bounds, sprite.uv(), tex_id, parent.z_index, type: sprite.type)!;
}

97
src/widgets/text.c3 Normal file
View File

@ -0,0 +1,97 @@
module ugui;
import std::io;
struct ElemText {
Id hash;
TextSize size;
TextEdit* te;
}
/* Layout some text without bounds.
* There is a limitation where the current frame bounds are based on the last frame, this is usually
* not a problem but it is in the situation where the text changes almost all frames.
*/
macro Ctx.text(&ctx, String text, ...)
=> ctx.text_id(@compute_id($vasplat), text);
fn void? Ctx.text_id(&ctx, Id id, String text)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_TEXT)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("text"));
Id text_hash = text.hash();
if (elem.flags.is_new || elem.text.hash != text_hash) {
elem.text.size = ctx.measure_string(text)!;
}
elem.text.hash = text_hash;
elem.layout.w = @fit(style.size);
elem.layout.h = @fit(style.size);
elem.layout.text = elem.text.size;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
ctx.layout_string(text, elem.bounds.pad(elem.layout.content_offset), TOP_LEFT, parent.z_index, style.fg)!;
}
macro Ctx.text_box(&ctx, Size w, Size h, TextEdit* te, Anchor text_alignment = TOP_LEFT, bool reflow = true, ...)
=> ctx.text_box_id(@compute_id($vasplat), w, h, te, text_alignment, reflow);
fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Size w, Size h, TextEdit* te, Anchor text_alignment, bool reflow)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_TEXT)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("text-box"));
elem.text.te = te;
Id text_hash = te.to_string().hash();
if (elem.flags.is_new || elem.text.hash != text_hash) {
elem.text.size = ctx.measure_string(te.to_string())!;
}
elem.text.hash = text_hash;
elem.layout.w = w;
elem.layout.h = h;
elem.layout.text = elem.text.size;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
// check input and update the text
//elem.events = ctx.get_elem_events(elem);
if (elem.events.text_input || elem.events.key_press) {
ctx.text_edit(elem.text.te);
}
Rect bg_bounds = elem.bounds.pad(style.margin);
Rect text_bounds = elem.bounds.pad(elem.layout.content_offset);
ctx.push_rect(bg_bounds, parent.z_index, style)!;
String s = elem.text.te.to_string();
if (te.sel_len) {
usz start = te.sel_len > 0 ? te.cursor : te.cursor + te.sel_len;
usz end = (te.sel_len > 0 ? te.cursor + te.sel_len : te.cursor) - 1;
ctx.draw_string_selection(s, text_bounds, text_alignment, start, end, parent.z_index, style.accent, reflow)!;
}
ctx.layout_string(s, text_bounds, text_alignment, parent.z_index, style.fg, reflow)!;
// draw the cursor if the element has focus
if (elem.events.has_focus) {
if (elem.events.mouse_press || elem.events.mouse_hold) {
usz cur = ctx.hit_test_string(s, text_bounds, text_alignment, ctx.input.mouse.pos, reflow)!;
bool select = (elem.events.mouse_hold && !elem.events.mouse_press) || (ctx.get_mod() & KMOD_SHIFT);
te.set_cursor(cur, select);
}
Rect cur = ctx.get_cursor_position(s, text_bounds, text_alignment, te.cursor, reflow)!;
cur.w = 2;
ctx.push_rect(cur, parent.z_index, &&(Style){.bg = style.fg})!;
}
return elem.events;
}

View File

@ -1,11 +0,0 @@
CFLAGS = -Wall -Wextra -Wpedantic -std=c11 -g
LDFLAGS = -lSDL2 -lm
test: main.c ../ugui.c ../ugui.h ../def_style.h ../input.c
gcc ${CFLAGS} -c ../ugui.c -o ugui.o
gcc ${CFLAGS} -c ../input.c -o input.o
gcc ${CFLAGS} -c main.c -o main.o
gcc ${LDFLAGS} main.o ugui.o input.o -o test
clean:
rm -f main.o ugui.o

View File

@ -1,268 +0,0 @@
#define _POSIX_C_SOURCE 200809l
#include <stdio.h>
#include <SDL2/SDL.h>
#include "../ugui.h"
#define STB_RECT_PACK_IMPLEMENTATION
#define STB_TRUETYPE_IMPLEMENTATION
#define STBTTF_IMPLEMENTATION
#include "stbttf.h"
SDL_Window *w;
SDL_Renderer *r;
ug_ctx_t *ctx;
STBTTF_Font *font;
void cleanup(void);
int main(void)
{
SDL_DisplayMode dm;
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
SDL_EnableScreenSaver();
SDL_EventState(SDL_DROPFILE, SDL_ENABLE);
SDL_EventState(SDL_DROPTEXT, SDL_ENABLE);
SDL_GetDesktopDisplayMode(0, &dm);
#ifdef SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR /* Available since 2.0.8 */
SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0");
#endif
#if SDL_VERSION_ATLEAST(2, 0, 5)
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
#endif
w = SDL_CreateWindow("test",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
dm.w*0.8, dm.h*0.8,
SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI |
SDL_WINDOW_OPENGL );
//SDL_Surface *s;
//s = SDL_GetWindowSurface(w);
r = SDL_CreateRenderer(w, -1, SDL_RENDERER_ACCELERATED);
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
ug_vec2_t size, dsize;
SDL_GetWindowSize(w, &size.w, &size.h);
SDL_GL_GetDrawableSize(w, &dsize.w, &dsize.h);
float scale = 1.0;
scale = ((float)(size.w+size.h)/2)/((float)(dsize.w+dsize.h)/2);
float dpi;
int idx;
idx = SDL_GetWindowDisplayIndex(w);
SDL_GetDisplayDPI(idx, &dpi, NULL, NULL);
ctx = ug_ctx_new();
ug_ctx_set_displayinfo(ctx, scale, dpi);
ug_ctx_set_drawableregion(ctx, dsize);
// open font
font = STBTTF_OpenFont(r, "monospace.ttf", ctx->style_px->title.font_size.size.i);
// atexit(cleanup);
SDL_Event event;
char button_map[] = {
[SDL_BUTTON_LEFT & 0xff] = UG_BTN_LEFT,
[SDL_BUTTON_MIDDLE & 0xff] = UG_BTN_MIDDLE,
[SDL_BUTTON_RIGHT & 0xff] = UG_BTN_RIGHT,
[SDL_BUTTON_X1 & 0xff] = UG_BTN_4,
[SDL_BUTTON_X2 & 0xff] = UG_BTN_5,
};
do {
SDL_WaitEvent(&event);
switch (event.type) {
case SDL_WINDOWEVENT:
switch (event.window.event) {
case SDL_WINDOWEVENT_SHOWN:
(SDL_Log("Window %d shown", event.window.windowID));
break;
case SDL_WINDOWEVENT_HIDDEN:
(SDL_Log("Window %d hidden", event.window.windowID));
break;
case SDL_WINDOWEVENT_EXPOSED:
(SDL_Log("Window %d exposed", event.window.windowID));
break;
case SDL_WINDOWEVENT_MOVED:
(SDL_Log("Window %d moved to %d,%d",
event.window.windowID, event.window.data1,
event.window.data2));
break;
case SDL_WINDOWEVENT_RESIZED:
(SDL_Log("Window %d resized to %dx%d",
event.window.windowID, event.window.data1,
event.window.data2));
break;
case SDL_WINDOWEVENT_SIZE_CHANGED:
(SDL_Log("Window %d size changed to %dx%d",
event.window.windowID, event.window.data1,
event.window.data2));
size.w = event.window.data1;
size.h = event.window.data2;
// surface is invalidated every time the window
// is resized
//s = SDL_GetWindowSurface(w);
ug_ctx_set_drawableregion(ctx, size);
break;
case SDL_WINDOWEVENT_MINIMIZED:
(SDL_Log("Window %d minimized", event.window.windowID));
break;
case SDL_WINDOWEVENT_MAXIMIZED:
(SDL_Log("Window %d maximized", event.window.windowID));
break;
case SDL_WINDOWEVENT_RESTORED:
(SDL_Log("Window %d restored", event.window.windowID));
break;
case SDL_WINDOWEVENT_ENTER:
(SDL_Log("Mouse entered window %d",
event.window.windowID));
break;
case SDL_WINDOWEVENT_LEAVE:
(SDL_Log("Mouse left window %d", event.window.windowID));
break;
case SDL_WINDOWEVENT_FOCUS_GAINED:
(SDL_Log("Window %d gained keyboard focus",
event.window.windowID));
break;
case SDL_WINDOWEVENT_FOCUS_LOST:
(SDL_Log("Window %d lost keyboard focus",
event.window.windowID));
break;
case SDL_WINDOWEVENT_CLOSE:
(SDL_Log("Window %d closed", event.window.windowID));
break;
#if SDL_VERSION_ATLEAST(2, 0, 5)
case SDL_WINDOWEVENT_TAKE_FOCUS:
(SDL_Log("Window %d is offered a focus", event.window.windowID));
break;
case SDL_WINDOWEVENT_HIT_TEST:
(SDL_Log("Window %d has a special hit test", event.window.windowID));
break;
#endif
default:
(SDL_Log("Window %d got unknown event %d",
event.window.windowID, event.window.event));
break;
}
break;
case SDL_QUIT:
(printf("Quitting\n"));
break;
case SDL_MOUSEMOTION:
ug_input_mousemove(ctx, event.motion.x, event.motion.y);
break;
case SDL_MOUSEBUTTONDOWN:
ug_input_mousemove(ctx, event.button.x, event.button.y);
ug_input_mousedown(ctx, button_map[event.button.button & 0xff]);
break;
case SDL_MOUSEBUTTONUP:
ug_input_mousemove(ctx, event.button.x, event.button.y);
ug_input_mouseup(ctx, button_map[event.button.button & 0xff]);
break;
default:
(printf("Unknown event: %d\n", event.type));
break;
}
ug_frame_begin(ctx);
//if (ctx->frame < 5000) {
// ug_container_menu_bar(ctx, "Menu fichissimo", (ug_size_t)SIZE_PX(24));
//} else if (ctx->frame == 5000) {
// ug_container_remove(ctx, "Menu fichissimo");
//}
ug_container_floating(ctx, "stupid name", (ug_div_t){.x=SIZE_PX(0), .y=SIZE_PX(0), .w=SIZE_PX(100), .h=SIZE_MM(75.0)});
ug_element_button(ctx, "float", "X", (ug_div_t){SQUARE(SIZE_MM(5))});
//ug_container_floating(ctx, "floating windoooooooow", (ug_div_t){.x=SIZE_PX(100), .y=SIZE_PX(0), .w=SIZE_PX(100), .h=SIZE_MM(75.0)});
//ug_container_sidebar(ctx, "Right Sidebar", (ug_size_t)SIZE_PX(300), UG_SIDE_RIGHT);
//ug_container_sidebar(ctx, "Left Sidebar", (ug_size_t)SIZE_PX(200), UG_SIDE_LEFT);
//ug_container_sidebar(ctx, "Bottom Sidebar", (ug_size_t)SIZE_MM(10), UG_SIDE_BOTTOM);
ug_container_sidebar(ctx, "Top Sidebar", (ug_size_t)SIZE_MM(40), UG_SIDE_TOP);
// ug_container_popup(ctx, "Annoying popup", (ug_div_t){.x=SIZE_MM(150), .y=SIZE_MM(150), .w=SIZE_PX(100), .h=SIZE_MM(75.0)});
//ug_container_body(ctx, "Main Body");
//if (ug_container_body(ctx, "Other Body"))
// printf("No space!\n");
ug_layout_row(ctx);
ug_layout_column(ctx);
ug_element_button(ctx, "button 1", "hey", (ug_div_t){SQUARE(SIZE_MM(10))});
//ug_element_button(ctx, "button 2", "lol", (ug_div_t){SQUARE(SIZE_MM(10))});
//ug_layout_next_row(ctx);
//ug_element_button(ctx, "button 3", "L", (ug_div_t){SQUARE(SIZE_MM(10))});
//ug_element_button(ctx, "button 4", "69", (ug_div_t){SQUARE(SIZE_MM(10))});
ug_layout_next_column(ctx);
ug_element_button(ctx, "button 5", "lmao", (ug_div_t){SQUARE(SIZE_MM(10))});
//ug_element_textbtn(ctx, "text button 1", "foo", (ug_div_t){.w = 0, .h = SIZE_PX(30)});
//ug_element_button(ctx, "button 6", "", (ug_div_t){SQUARE(SIZE_MM(10)),.x=SIZE_PX(-10)});
ug_container_body(ctx, "fill body");
ug_frame_end(ctx);
// fill background
// solid purple makes it easy to identify, same color as hl missing texture
SDL_SetRenderDrawColor(r, 0xff, 0, 0xdc, 0xff);
SDL_RenderClear(r);
for (ug_cmd_t *cmd = NULL; (cmd = ug_cmd_next(ctx));) {
switch (cmd->type) {
case UG_CMD_RECT:
{
ug_color_t col = cmd->rect.color;
SDL_Rect sr = {
.x = cmd->rect.x,
.y = cmd->rect.y,
.w = cmd->rect.w,
.h = cmd->rect.h,
};
SDL_SetRenderDrawColor(r, col.r, col.g, col.b, col.a);
SDL_RenderFillRect(r, &sr);
}
break;
case UG_CMD_TEXT:
SDL_SetRenderDrawColor(r, cmd->text.color.r, cmd->text.color.g, cmd->text.color.b, cmd->text.color.a);
STBTTF_RenderText(r, font, cmd->text.x, cmd->text.y, cmd->text.str);
break;
default: break;
}
}
SDL_RenderPresent(r);
} while (event.type != SDL_QUIT);
cleanup();
return 0;
}
void cleanup(void)
{
ug_ctx_free(ctx);
STBTTF_CloseFont(font);
SDL_DestroyRenderer(r);
SDL_DestroyWindow(w);
SDL_Quit();
}

Binary file not shown.

View File

@ -1,623 +0,0 @@
// stb_rect_pack.h - v1.01 - public domain - rectangle packing
// Sean Barrett 2014
//
// Useful for e.g. packing rectangular textures into an atlas.
// Does not do rotation.
//
// Before #including,
//
// #define STB_RECT_PACK_IMPLEMENTATION
//
// in the file that you want to have the implementation.
//
// Not necessarily the awesomest packing method, but better than
// the totally naive one in stb_truetype (which is primarily what
// this is meant to replace).
//
// Has only had a few tests run, may have issues.
//
// More docs to come.
//
// No memory allocations; uses qsort() and assert() from stdlib.
// Can override those by defining STBRP_SORT and STBRP_ASSERT.
//
// This library currently uses the Skyline Bottom-Left algorithm.
//
// Please note: better rectangle packers are welcome! Please
// implement them to the same API, but with a different init
// function.
//
// Credits
//
// Library
// Sean Barrett
// Minor features
// Martins Mozeiko
// github:IntellectualKitty
//
// Bugfixes / warning fixes
// Jeremy Jaussaud
// Fabian Giesen
//
// Version history:
//
// 1.01 (2021-07-11) always use large rect mode, expose STBRP__MAXVAL in public section
// 1.00 (2019-02-25) avoid small space waste; gracefully fail too-wide rectangles
// 0.99 (2019-02-07) warning fixes
// 0.11 (2017-03-03) return packing success/fail result
// 0.10 (2016-10-25) remove cast-away-const to avoid warnings
// 0.09 (2016-08-27) fix compiler warnings
// 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0)
// 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0)
// 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort
// 0.05: added STBRP_ASSERT to allow replacing assert
// 0.04: fixed minor bug in STBRP_LARGE_RECTS support
// 0.01: initial release
//
// LICENSE
//
// See end of file for license information.
//////////////////////////////////////////////////////////////////////////////
//
// INCLUDE SECTION
//
#ifndef STB_INCLUDE_STB_RECT_PACK_H
#define STB_INCLUDE_STB_RECT_PACK_H
#define STB_RECT_PACK_VERSION 1
#ifdef STBRP_STATIC
#define STBRP_DEF static
#else
#define STBRP_DEF extern
#endif
#ifdef __cplusplus
extern "C" {
#endif
typedef struct stbrp_context stbrp_context;
typedef struct stbrp_node stbrp_node;
typedef struct stbrp_rect stbrp_rect;
typedef int stbrp_coord;
#define STBRP__MAXVAL 0x7fffffff
// Mostly for internal use, but this is the maximum supported coordinate value.
STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects);
// Assign packed locations to rectangles. The rectangles are of type
// 'stbrp_rect' defined below, stored in the array 'rects', and there
// are 'num_rects' many of them.
//
// Rectangles which are successfully packed have the 'was_packed' flag
// set to a non-zero value and 'x' and 'y' store the minimum location
// on each axis (i.e. bottom-left in cartesian coordinates, top-left
// if you imagine y increasing downwards). Rectangles which do not fit
// have the 'was_packed' flag set to 0.
//
// You should not try to access the 'rects' array from another thread
// while this function is running, as the function temporarily reorders
// the array while it executes.
//
// To pack into another rectangle, you need to call stbrp_init_target
// again. To continue packing into the same rectangle, you can call
// this function again. Calling this multiple times with multiple rect
// arrays will probably produce worse packing results than calling it
// a single time with the full rectangle array, but the option is
// available.
//
// The function returns 1 if all of the rectangles were successfully
// packed and 0 otherwise.
struct stbrp_rect
{
// reserved for your use:
int id;
// input:
stbrp_coord w, h;
// output:
stbrp_coord x, y;
int was_packed; // non-zero if valid packing
}; // 16 bytes, nominally
STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes);
// Initialize a rectangle packer to:
// pack a rectangle that is 'width' by 'height' in dimensions
// using temporary storage provided by the array 'nodes', which is 'num_nodes' long
//
// You must call this function every time you start packing into a new target.
//
// There is no "shutdown" function. The 'nodes' memory must stay valid for
// the following stbrp_pack_rects() call (or calls), but can be freed after
// the call (or calls) finish.
//
// Note: to guarantee best results, either:
// 1. make sure 'num_nodes' >= 'width'
// or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1'
//
// If you don't do either of the above things, widths will be quantized to multiples
// of small integers to guarantee the algorithm doesn't run out of temporary storage.
//
// If you do #2, then the non-quantized algorithm will be used, but the algorithm
// may run out of temporary storage and be unable to pack some rectangles.
STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem);
// Optionally call this function after init but before doing any packing to
// change the handling of the out-of-temp-memory scenario, described above.
// If you call init again, this will be reset to the default (false).
STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic);
// Optionally select which packing heuristic the library should use. Different
// heuristics will produce better/worse results for different data sets.
// If you call init again, this will be reset to the default.
enum
{
STBRP_HEURISTIC_Skyline_default=0,
STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default,
STBRP_HEURISTIC_Skyline_BF_sortHeight
};
//////////////////////////////////////////////////////////////////////////////
//
// the details of the following structures don't matter to you, but they must
// be visible so you can handle the memory allocations for them
struct stbrp_node
{
stbrp_coord x,y;
stbrp_node *next;
};
struct stbrp_context
{
int width;
int height;
int align;
int init_mode;
int heuristic;
int num_nodes;
stbrp_node *active_head;
stbrp_node *free_head;
stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2'
};
#ifdef __cplusplus
}
#endif
#endif
//////////////////////////////////////////////////////////////////////////////
//
// IMPLEMENTATION SECTION
//
#ifdef STB_RECT_PACK_IMPLEMENTATION
#ifndef STBRP_SORT
#include <stdlib.h>
#define STBRP_SORT qsort
#endif
#ifndef STBRP_ASSERT
#include <assert.h>
#define STBRP_ASSERT assert
#endif
#ifdef _MSC_VER
#define STBRP__NOTUSED(v) (void)(v)
#define STBRP__CDECL __cdecl
#else
#define STBRP__NOTUSED(v) (void)sizeof(v)
#define STBRP__CDECL
#endif
enum
{
STBRP__INIT_skyline = 1
};
STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic)
{
switch (context->init_mode) {
case STBRP__INIT_skyline:
STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight);
context->heuristic = heuristic;
break;
default:
STBRP_ASSERT(0);
}
}
STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem)
{
if (allow_out_of_mem)
// if it's ok to run out of memory, then don't bother aligning them;
// this gives better packing, but may fail due to OOM (even though
// the rectangles easily fit). @TODO a smarter approach would be to only
// quantize once we've hit OOM, then we could get rid of this parameter.
context->align = 1;
else {
// if it's not ok to run out of memory, then quantize the widths
// so that num_nodes is always enough nodes.
//
// I.e. num_nodes * align >= width
// align >= width / num_nodes
// align = ceil(width/num_nodes)
context->align = (context->width + context->num_nodes-1) / context->num_nodes;
}
}
STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes)
{
int i;
for (i=0; i < num_nodes-1; ++i)
nodes[i].next = &nodes[i+1];
nodes[i].next = NULL;
context->init_mode = STBRP__INIT_skyline;
context->heuristic = STBRP_HEURISTIC_Skyline_default;
context->free_head = &nodes[0];
context->active_head = &context->extra[0];
context->width = width;
context->height = height;
context->num_nodes = num_nodes;
stbrp_setup_allow_out_of_mem(context, 0);
// node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly)
context->extra[0].x = 0;
context->extra[0].y = 0;
context->extra[0].next = &context->extra[1];
context->extra[1].x = (stbrp_coord) width;
context->extra[1].y = (1<<30);
context->extra[1].next = NULL;
}
// find minimum y position if it starts at x1
static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0, int width, int *pwaste)
{
stbrp_node *node = first;
int x1 = x0 + width;
int min_y, visited_width, waste_area;
STBRP__NOTUSED(c);
STBRP_ASSERT(first->x <= x0);
#if 0
// skip in case we're past the node
while (node->next->x <= x0)
++node;
#else
STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency
#endif
STBRP_ASSERT(node->x <= x0);
min_y = 0;
waste_area = 0;
visited_width = 0;
while (node->x < x1) {
if (node->y > min_y) {
// raise min_y higher.
// we've accounted for all waste up to min_y,
// but we'll now add more waste for everything we've visted
waste_area += visited_width * (node->y - min_y);
min_y = node->y;
// the first time through, visited_width might be reduced
if (node->x < x0)
visited_width += node->next->x - x0;
else
visited_width += node->next->x - node->x;
} else {
// add waste area
int under_width = node->next->x - node->x;
if (under_width + visited_width > width)
under_width = width - visited_width;
waste_area += under_width * (min_y - node->y);
visited_width += under_width;
}
node = node->next;
}
*pwaste = waste_area;
return min_y;
}
typedef struct
{
int x,y;
stbrp_node **prev_link;
} stbrp__findresult;
static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height)
{
int best_waste = (1<<30), best_x, best_y = (1 << 30);
stbrp__findresult fr;
stbrp_node **prev, *node, *tail, **best = NULL;
// align to multiple of c->align
width = (width + c->align - 1);
width -= width % c->align;
STBRP_ASSERT(width % c->align == 0);
// if it can't possibly fit, bail immediately
if (width > c->width || height > c->height) {
fr.prev_link = NULL;
fr.x = fr.y = 0;
return fr;
}
node = c->active_head;
prev = &c->active_head;
while (node->x + width <= c->width) {
int y,waste;
y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste);
if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL
// bottom left
if (y < best_y) {
best_y = y;
best = prev;
}
} else {
// best-fit
if (y + height <= c->height) {
// can only use it if it first vertically
if (y < best_y || (y == best_y && waste < best_waste)) {
best_y = y;
best_waste = waste;
best = prev;
}
}
}
prev = &node->next;
node = node->next;
}
best_x = (best == NULL) ? 0 : (*best)->x;
// if doing best-fit (BF), we also have to try aligning right edge to each node position
//
// e.g, if fitting
//
// ____________________
// |____________________|
//
// into
//
// | |
// | ____________|
// |____________|
//
// then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned
//
// This makes BF take about 2x the time
if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) {
tail = c->active_head;
node = c->active_head;
prev = &c->active_head;
// find first node that's admissible
while (tail->x < width)
tail = tail->next;
while (tail) {
int xpos = tail->x - width;
int y,waste;
STBRP_ASSERT(xpos >= 0);
// find the left position that matches this
while (node->next->x <= xpos) {
prev = &node->next;
node = node->next;
}
STBRP_ASSERT(node->next->x > xpos && node->x <= xpos);
y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste);
if (y + height <= c->height) {
if (y <= best_y) {
if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) {
best_x = xpos;
STBRP_ASSERT(y <= best_y);
best_y = y;
best_waste = waste;
best = prev;
}
}
}
tail = tail->next;
}
}
fr.prev_link = best;
fr.x = best_x;
fr.y = best_y;
return fr;
}
static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height)
{
// find best position according to heuristic
stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height);
stbrp_node *node, *cur;
// bail if:
// 1. it failed
// 2. the best node doesn't fit (we don't always check this)
// 3. we're out of memory
if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) {
res.prev_link = NULL;
return res;
}
// on success, create new node
node = context->free_head;
node->x = (stbrp_coord) res.x;
node->y = (stbrp_coord) (res.y + height);
context->free_head = node->next;
// insert the new node into the right starting point, and
// let 'cur' point to the remaining nodes needing to be
// stiched back in
cur = *res.prev_link;
if (cur->x < res.x) {
// preserve the existing one, so start testing with the next one
stbrp_node *next = cur->next;
cur->next = node;
cur = next;
} else {
*res.prev_link = node;
}
// from here, traverse cur and free the nodes, until we get to one
// that shouldn't be freed
while (cur->next && cur->next->x <= res.x + width) {
stbrp_node *next = cur->next;
// move the current node to the free list
cur->next = context->free_head;
context->free_head = cur;
cur = next;
}
// stitch the list back in
node->next = cur;
if (cur->x < res.x + width)
cur->x = (stbrp_coord) (res.x + width);
#ifdef _DEBUG
cur = context->active_head;
while (cur->x < context->width) {
STBRP_ASSERT(cur->x < cur->next->x);
cur = cur->next;
}
STBRP_ASSERT(cur->next == NULL);
{
int count=0;
cur = context->active_head;
while (cur) {
cur = cur->next;
++count;
}
cur = context->free_head;
while (cur) {
cur = cur->next;
++count;
}
STBRP_ASSERT(count == context->num_nodes+2);
}
#endif
return res;
}
static int STBRP__CDECL rect_height_compare(const void *a, const void *b)
{
const stbrp_rect *p = (const stbrp_rect *) a;
const stbrp_rect *q = (const stbrp_rect *) b;
if (p->h > q->h)
return -1;
if (p->h < q->h)
return 1;
return (p->w > q->w) ? -1 : (p->w < q->w);
}
static int STBRP__CDECL rect_original_order(const void *a, const void *b)
{
const stbrp_rect *p = (const stbrp_rect *) a;
const stbrp_rect *q = (const stbrp_rect *) b;
return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed);
}
STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects)
{
int i, all_rects_packed = 1;
// we use the 'was_packed' field internally to allow sorting/unsorting
for (i=0; i < num_rects; ++i) {
rects[i].was_packed = i;
}
// sort according to heuristic
STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare);
for (i=0; i < num_rects; ++i) {
if (rects[i].w == 0 || rects[i].h == 0) {
rects[i].x = rects[i].y = 0; // empty rect needs no space
} else {
stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h);
if (fr.prev_link) {
rects[i].x = (stbrp_coord) fr.x;
rects[i].y = (stbrp_coord) fr.y;
} else {
rects[i].x = rects[i].y = STBRP__MAXVAL;
}
}
}
// unsort
STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order);
// set was_packed flags and all_rects_packed status
for (i=0; i < num_rects; ++i) {
rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL);
if (!rects[i].was_packed)
all_rects_packed = 0;
}
// return the all_rects_packed status
return all_rects_packed;
}
#endif
/*
------------------------------------------------------------------------------
This software is available under 2 licenses -- choose whichever you prefer.
------------------------------------------------------------------------------
ALTERNATIVE A - MIT License
Copyright (c) 2017 Sean Barrett
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
ALTERNATIVE B - Public Domain (www.unlicense.org)
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
------------------------------------------------------------------------------
*/

File diff suppressed because it is too large Load Diff

View File

@ -1,212 +0,0 @@
#ifndef __STBTTF_H__
#define __STBTTF_H__
#include <SDL2/SDL.h>
#include "stb_rect_pack.h"
#include "stb_truetype.h"
/* STBTTF: A quick and dirty SDL2 text renderer based on stb_truetype and stdb_rect_pack.
* Benoit Favre 2019
*
* This header-only addon to the stb_truetype library allows to draw text with SDL2 from
* TTF fonts with a similar API to SDL_TTF without the bloat.
* The renderer is however limited by the integral positioning of SDL blit functions.
* It also does not parse utf8 text and only prints ASCII characters.
*
* This code is public domain.
*/
typedef struct {
stbtt_fontinfo* info;
stbtt_packedchar* chars;
SDL_Texture* atlas;
int texture_size;
float size;
float scale;
int ascent;
int baseline;
} STBTTF_Font;
/* Release the memory and textures associated with a font */
void STBTTF_CloseFont(STBTTF_Font* font);
/* Open a TTF font given a SDL abstract IO handler, for a given renderer and a given font size.
* Returns NULL on failure. The font must be deallocated with STBTTF_CloseFont when not used anymore.
* This function creates a texture atlas with prerendered ASCII characters (32-128).
*/
STBTTF_Font* STBTTF_OpenFontRW(SDL_Renderer* renderer, SDL_RWops* rw, float size);
/* Open a TTF font given a filename, for a given renderer and a given font size.
* Convinience function which calls STBTTF_OpenFontRW.
*/
STBTTF_Font* STBTTF_OpenFont(SDL_Renderer* renderer, const char* filename, float size);
/* Draw some text using the renderer draw color at location (x, y).
* Characters are copied from the texture atlas using the renderer SDL_RenderCopy function.
* Since that function only supports integral coordinates, the result is not great.
* Only ASCII characters (32 <= c < 128) are supported. Anything outside this range is ignored.
*/
void STBTTF_RenderText(SDL_Renderer* renderer, STBTTF_Font* font, float x, float y, const char *text);
/* Return the length in pixels of a text.
* You can get the height of a line by using font->baseline.
*/
float STBTTF_MeasureText(STBTTF_Font* font, const char *text);
#ifdef STBTTF_IMPLEMENTATION
void STBTTF_CloseFont(STBTTF_Font* font) {
if(font->atlas) SDL_DestroyTexture(font->atlas);
if(font->info) free(font->info);
if(font->chars) free(font->chars);
free(font);
}
STBTTF_Font* STBTTF_OpenFontRW(SDL_Renderer* renderer, SDL_RWops* rw, float size) {
Sint64 file_size = SDL_RWsize(rw);
unsigned char* buffer = malloc(file_size);
if(SDL_RWread(rw, buffer, file_size, 1) != 1) return NULL;
SDL_RWclose(rw);
STBTTF_Font* font = calloc(sizeof(STBTTF_Font), 1);
font->info = malloc(sizeof(stbtt_fontinfo));
font->chars = malloc(sizeof(stbtt_packedchar) * 96);
if(stbtt_InitFont(font->info, buffer, 0) == 0) {
free(buffer);
STBTTF_CloseFont(font);
return NULL;
}
// fill bitmap atlas with packed characters
unsigned char* bitmap = NULL;
font->texture_size = 32;
while(1) {
bitmap = malloc(font->texture_size * font->texture_size);
stbtt_pack_context pack_context;
stbtt_PackBegin(&pack_context, bitmap, font->texture_size, font->texture_size, 0, 1, 0);
stbtt_PackSetOversampling(&pack_context, 1, 1);
if(!stbtt_PackFontRange(&pack_context, buffer, 0, size, 32, 95, font->chars)) {
// too small
free(bitmap);
stbtt_PackEnd(&pack_context);
font->texture_size *= 2;
} else {
stbtt_PackEnd(&pack_context);
break;
}
}
// convert bitmap to texture
font->atlas = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STATIC, font->texture_size, font->texture_size);
SDL_SetTextureBlendMode(font->atlas, SDL_BLENDMODE_BLEND);
Uint32* pixels = malloc(font->texture_size * font->texture_size * sizeof(Uint32));
static SDL_PixelFormat* format = NULL;
if(format == NULL) format = SDL_AllocFormat(SDL_PIXELFORMAT_RGBA32);
for(int i = 0; i < font->texture_size * font->texture_size; i++) {
pixels[i] = SDL_MapRGBA(format, 0xff, 0xff, 0xff, bitmap[i]);
}
SDL_UpdateTexture(font->atlas, NULL, pixels, font->texture_size * sizeof(Uint32));
free(pixels);
free(bitmap);
// setup additional info
font->scale = stbtt_ScaleForPixelHeight(font->info, size);
stbtt_GetFontVMetrics(font->info, &font->ascent, 0, 0);
font->baseline = (int) (font->ascent * font->scale);
free(buffer);
return font;
}
STBTTF_Font* STBTTF_OpenFont(SDL_Renderer* renderer, const char* filename, float size) {
SDL_RWops *rw = SDL_RWFromFile(filename, "rb");
if(rw == NULL) return NULL;
return STBTTF_OpenFontRW(renderer, rw, size);
}
void STBTTF_RenderText(SDL_Renderer* renderer, STBTTF_Font* font, float x, float y, const char *text) {
Uint8 r, g, b, a;
SDL_GetRenderDrawColor(renderer, &r, &g, &b, &a);
SDL_SetTextureColorMod(font->atlas, r, g, b);
SDL_SetTextureAlphaMod(font->atlas, a);
for(int i = 0; text[i]; i++) {
if (text[i] >= 32 && text[i] < 128) {
//if(i > 0) x += stbtt_GetCodepointKernAdvance(font->info, text[i - 1], text[i]) * font->scale;
stbtt_packedchar* info = &font->chars[text[i] - 32];
SDL_Rect src_rect = {info->x0, info->y0, info->x1 - info->x0, info->y1 - info->y0};
SDL_Rect dst_rect = {x + info->xoff, y + info->yoff, info->x1 - info->x0, info->y1 - info->y0};
SDL_RenderCopy(renderer, font->atlas, &src_rect, &dst_rect);
x += info->xadvance;
}
}
}
float STBTTF_MeasureText(STBTTF_Font* font, const char *text) {
float width = 0;
for(int i = 0; text[i]; i++) {
if (text[i] >= 32 && text[i] < 128) {
//if(i > 0) width += stbtt_GetCodepointKernAdvance(font->info, text[i - 1], text[i]) * font->scale;
stbtt_packedchar* info = &font->chars[text[i] - 32];
width += info->xadvance;
}
}
return width;
}
/*******************
* Example program *
*******************
#include <stdio.h>
#define STB_RECT_PACK_IMPLEMENTATION
#define STB_TRUETYPE_IMPLEMENTATION
#define STBTTF_IMPLEMENTATION
#include "stbttf.h"
int main(int argc, char** argv) {
if(argc != 2) {
fprintf(stderr, "usage: %s <font>\n", argv[0]);
exit(1);
}
SDL_Init(SDL_INIT_VIDEO);
SDL_Window* window = SDL_CreateWindow("stbttf", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL);
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
SDL_RenderSetLogicalSize(renderer, 640, 480);
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
STBTTF_Font* font = STBTTF_OpenFont(renderer, argv[1], 32);
while(1) {
SDL_Event event;
while(SDL_PollEvent(&event)) {
if(event.type == SDL_QUIT) exit(0);
}
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
// set color and render some text
SDL_SetRenderDrawColor(renderer, 128, 0, 0, 255);
STBTTF_RenderText(renderer, font, 50, 50, "This is a test");
// render the atlas to check its content
//SDL_Rect dest = {0, 0, font->texturesize, font->texturesize};
//SDL_RenderCopy(renderer, font->atlas, &dest, &dest);
SDL_RenderPresent(renderer);
SDL_Delay(1000 / 60);
}
STBTTF_CloseFont(font);
SDL_Quit();
}
*/
#endif
#endif

View File

@ -1,4 +0,0 @@
*.png
test
obj/**
objlist

View File

@ -1,18 +0,0 @@
CC = gcc
# instrumentation flags
INFLAGS = -fsanitize=address,builtin,undefined
LDFLAGS = -lm -lgrapheme -lSDL2 -lGLEW -lGL ${INFLAGS}
CFLAGS = -ggdb3 -Wall -Wextra -pedantic -std=c11 -Wno-unused-function -fno-omit-frame-pointer ${INFLAGS}
.PHONY: clean all
all: test
font.o: font.c font.h stb_truetype.h stb_image_write.h util.h \
generic_cache.h generic_hash.h
main.o: main.c ren.h util.h
ren.o: ren.c util.h font.h ren.h generic_stack.h
util.o: util.c util.h
test: font.o main.o ren.o util.o
${CC} ${LDFLAGS} -o test font.o main.o ren.o util.o
clean:
rm -f test font.o main.o ren.o util.o

View File

@ -1,8 +0,0 @@
#version 330 core
in vec4 col;
void main()
{
gl_FragColor = col;
}

View File

@ -1,19 +0,0 @@
#version 330 core
uniform ivec2 viewsize;
layout(location = 0) in vec2 position;
layout(location = 2) in vec4 color;
out vec4 col;
void main()
{
vec2 v = vec2(float(viewsize.x), float(viewsize.y));
vec2 p = vec2(position.x*2.0/v.x - 1.0, 1.0 - position.y*2.0/v.y);
vec4 pos = vec4(p.x, p.y, 0.0f, 1.0f);
gl_Position = pos;
col = color;
}

View File

@ -1,195 +0,0 @@
#define _POSIX_C_SOURCE 200809l
#define STB_TRUETYPE_IMPLEMENTATION
#define STBTT_STATIC
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <grapheme.h>
#include <assert.h>
#include "font.h"
#include "stb_truetype.h"
#include "stb_image_write.h"
#include "util.h"
#include "generic_cache.h"
static inline unsigned int hash(unsigned int code)
{
// identity map the ascii range
return code < 128 ? code : hash_u32(code);
}
CACHE_DECL(cache, struct font_glyph, hash, hash_cmp_u32)
#define UTF8(c) (c&0x80)
#define BDEPTH 1
#define BORDER 4
// FIXME: as of now only monospaced fonts work correctly since no kerning information
// is stored
struct priv {
stbtt_fontinfo stb;
float scale;
int baseline;
unsigned char *bitmap;
struct cache c;
};
#define PRIV(x) ((struct priv *)x->priv)
struct font_atlas * font_init(void)
{
struct font_atlas *p = emalloc(sizeof(struct font_atlas));
memset(p, 0, sizeof(struct font_atlas));
p->priv = emalloc(sizeof(struct priv));
memset(p->priv, 0, sizeof(struct priv));
PRIV(p)->c = cache_init();
return p;
}
// loads a font into memory, storing all the ASCII characters in the atlas, each font
// atlas structure holds glyphs of a specific size in pixels
// NOTE: size includes ascend and descend (so 12 does not mean that 'A' is 12px tall)
int font_load(struct font_atlas *atlas, const char *path, int size)
{
if (!atlas || !path)
return -1;
int err;
dump_file(path, &(atlas->file), &(atlas->file_size));
err = stbtt_InitFont(&(PRIV(atlas)->stb), (unsigned char *)atlas->file, 0);
if (err == 0) return -1;
int ascent, descent, linegap, baseline;
int x0,y0,x1,y1;
float scale;
stbtt_GetFontVMetrics(&(PRIV(atlas)->stb), &ascent, &descent, &linegap);
stbtt_GetFontBoundingBox(&(PRIV(atlas)->stb), &x0, &y0, &x1, &y1);
scale = stbtt_ScaleForPixelHeight(&(PRIV(atlas)->stb), size);
baseline = scale * -y0;
atlas->glyph_max_w = (scale*x1) - (scale*x0);
atlas->glyph_max_h = (baseline+scale*y1) - (baseline+scale*y0);
atlas->atlas = emalloc(CACHE_SIZE*BDEPTH*atlas->glyph_max_w*atlas->glyph_max_h);
memset(atlas->atlas, 0, CACHE_SIZE*BDEPTH*atlas->glyph_max_w*atlas->glyph_max_h);
PRIV(atlas)->baseline = atlas->glyph_max_h - baseline;
PRIV(atlas)->scale = scale;
PRIV(atlas)->bitmap = emalloc(BDEPTH*atlas->glyph_max_w*atlas->glyph_max_h);
// FIXME: make this a square atlas
atlas->width = atlas->glyph_max_w*CACHE_SIZE/4;
atlas->height = atlas->glyph_max_h*4;
atlas->size = size;
// preallocate all ascii characters
for (char c = ' '; c <= '~'; c++) {
if (!font_get_glyph_texture(atlas, c, NULL))
return -1;
}
return 0;
}
int font_free(struct font_atlas *atlas)
{
efree(atlas->atlas);
efree(atlas->file);
efree(PRIV(atlas)->bitmap);
cache_free(&PRIV(atlas)->c);
efree(atlas->priv);
efree(atlas);
return 0;
}
// TODO: time and take the median of the time it takes to generate the cache and
// the time it takes to draw the glyph
const struct font_glyph * font_get_glyph_texture(struct font_atlas *atlas, unsigned int code, int *updated)
{
int _u = 0;
if (!updated) updated = &_u;
const struct font_glyph *r;
if ((r = cache_search(&PRIV(atlas)->c, code)) != NULL) {
*updated = 0;
return r;
}
*updated = 1;
// generate the sdf and put it into the cache
// TODO: generate the whole block at once
int idx = stbtt_FindGlyphIndex(&PRIV(atlas)->stb, code);
int x0,y0,x1,y1,gw,gh,l,off_x,off_y,adv,base;
base = atlas->glyph_max_h - PRIV(atlas)->baseline;
stbtt_GetGlyphBitmapBoxSubpixel(
&PRIV(atlas)->stb,
idx,
PRIV(atlas)->scale,
PRIV(atlas)->scale,
0,0,
&x0,&y0,
&x1, &y1);
gw = x1 - x0;
gh = y1 - y0;
stbtt_GetGlyphHMetrics(&PRIV(atlas)->stb, idx, &adv, &l);
adv *= PRIV(atlas)->scale;
off_x = PRIV(atlas)->scale*l;
off_y = atlas->glyph_max_h+y0;
stbtt_MakeGlyphBitmapSubpixel(
&PRIV(atlas)->stb,
PRIV(atlas)->bitmap,
atlas->glyph_max_w,
atlas->glyph_max_h,
atlas->glyph_max_w,
PRIV(atlas)->scale,
PRIV(atlas)->scale,
0, 0,
idx);
// TODO: bounds check usign atlas height
// TODO: clear spot area in the atlas before writing on it
unsigned int spot = cache_get_free_spot(&PRIV(atlas)->c);
unsigned int ty = ((atlas->glyph_max_w * spot) / atlas->width) * atlas->glyph_max_h;
unsigned int tx = (atlas->glyph_max_w * spot) % atlas->width;
unsigned int w = atlas->width;
unsigned char *a = (void *)atlas->atlas;
//printf("max:%d %d spot:%d : %d %d %d %d\n", atlas->glyph_max_w, atlas->glyph_max_h, spot, tx, ty, off_x, off_y);
for (int y = 0; y < gh; y++) {
for (int x = 0; x < gw; x++) {
int c, r;
r = (ty+y)*w;
c = tx+x;
a[r+c] = PRIV(atlas)->bitmap[y*atlas->glyph_max_w+x];
}
}
struct font_glyph g = {
.codepoint = code,
.u = tx,
.v = ty,
.w = gw,
.h = gh,
.x = off_x,
.y = off_y-base,
.a = adv,
};
return cache_insert_at(&PRIV(atlas)->c, &g, g.codepoint, spot);
}
void font_dump(const struct font_atlas *atlas, const char *path)
{
stbi_write_png(
path,
atlas->width,
atlas->height,
BDEPTH,
atlas->atlas,
BDEPTH*atlas->width);
}

View File

@ -1,50 +0,0 @@
#ifndef _FONT_H
#define _FONT_H
#include <stdint.h>
/* width and height of a glyph contain the kering advance
* (u,v)
* +-------------*---+ -
* | ^ | | ^
* | |oy | | |
* | v | | |
* | .ii. | | |
* | @@@@@@. |<->| |
* | V@Mio@@o |adv| |
* | :i. V@V | | |
* | :oM@@M | | |
* | :@@@MM@M | | |
* | @@o o@M | | |
* |<->:@@. M@M | | |
* |ox @@@o@@@@ | | |
* | :M@@V:@@.| | v
* +-------------*---+ -
* |<------------->|
* w
*/
// TODO: the advance isn't unique for every pair of characters
struct font_glyph {
uint32_t codepoint;
uint32_t u, v;
uint16_t w, h, a, x, y;
};
struct font_atlas {
unsigned int width, height;
unsigned char *atlas;
unsigned int glyph_max_w, glyph_max_h;
int size;
int file_size;
char *file;
void *priv;
};
struct font_atlas * font_init(void);
int font_load(struct font_atlas *atlas, const char *path, int size);
int font_free(struct font_atlas *atlas);
const struct font_glyph * font_get_glyph_texture(struct font_atlas *atlas, unsigned int code, int *updated);
void font_dump(const struct font_atlas *atlas, const char *path);
#endif

View File

@ -1,18 +0,0 @@
#version 330 core
// viewsize.x = viewport width in pixels; viewsize.y = viewport height in pixels
uniform ivec2 viewsize;
uniform ivec2 texturesize;
// texture uv coordinate in texture space
in vec2 uv;
uniform sampler2DRect ts;
const vec3 textcolor = vec3(1.0, 1.0, 1.0);
void main()
{
//gl_FragColor = vec4(1.0f,0.0f,0.0f,1.0f);
gl_FragColor = vec4(textcolor, texture(ts, uv));
}

View File

@ -1,26 +0,0 @@
#version 330 core
// viewsize.x = viewport width in pixels; viewsize.y = viewport height in pixels
uniform ivec2 viewsize;
uniform ivec2 texturesize;
// both position and and uv are in pixels, they where converted to floats when
// passed to the shader
layout(location = 0) in vec2 position;
layout(location = 1) in vec2 txcoord;
out vec2 uv;
void main()
{
vec2 v = vec2(float(viewsize.x), float(viewsize.y));
// vec2 p = vec2(position.x*2.0f/v.x - 1.0f, position.y*2.0f/v.y - 1.0f);
vec2 p = vec2(position.x*2.0/v.x - 1.0, 1.0 - position.y*2.0/v.y);
vec4 pos = vec4(p.x, p.y, 0.0f, 1.0f);
gl_Position = pos;
// since the texture is a GL_TEXTURE_RECTANGLE the coordintes do not need to
// be normalized
uv = vec2(txcoord.x, txcoord.y);
}

View File

@ -1,126 +0,0 @@
#ifndef CACHE_GENERIC_H
#define CACHE_GENERIC_H
#include "generic_hash.h"
#define CACHE_SIZE 512
#define CACHE_NCYCLES (CACHE_SIZE*2/3)
#define CACHE_BSIZE (((CACHE_SIZE+0x3f)&(~0x3f))>>6)
#define CACHE_BRESET(b) for (int i = 0; i < CACHE_BSIZE; b[i++] = 0);
#define CACHE_BSET(b, x) b[(x)>>6] |= (uint64_t)1<<((x)&63)
#define CACHE_BTEST(b, x) (b[(x)>>6]&((uint64_t)1<<((x)&63)))
// FIXME: this cache implementation is not really generic since it expects an unsigned
// as the code and not a generic type
#define CACHE_CYCLE(c) \
{ \
if (++(c->cycles) > CACHE_NCYCLES) { \
for (int i = 0; i < CACHE_BSIZE; i++) { \
c->present[i] &= c->used[i]; \
c->used[i] = 0; \
} \
c->cycles = 0; \
} \
}
#define CACHE_DECL(name, type, hashfn, cmpfn) \
HASH_DECL(name##table, uint32_t, uint32_t, hashfn, cmpfn) \
struct name { \
struct name##table_ref *table; \
type *array; \
uint64_t *present, *used; \
int cycles; \
}; \
\
\
/* FIXME: check for allocation errors */ \
struct name name##_init(void) \
{ \
struct name##table_ref *t = name##table_create(CACHE_SIZE); \
type *a = malloc(sizeof(type)*CACHE_SIZE); \
uint64_t *p = malloc(sizeof(uint64_t)*CACHE_BSIZE); \
uint64_t *u = malloc(sizeof(uint64_t)*CACHE_BSIZE); \
CACHE_BRESET(p); \
CACHE_BRESET(u); \
return (struct name){.table=t, .array=a, .present=p, .used=u, 0}; \
} \
\
\
void name##_free(struct name *cache) \
{ \
if (cache) { \
name##table_destroy(cache->table); \
free(cache->array); \
free(cache->present); \
free(cache->used); \
} \
} \
\
\
const type * name##_search(struct name *cache, uint32_t code) \
{ \
if (!cache) return NULL; \
struct name##table_entry *r; \
r = name##table_search(cache->table, code); \
/* MISS */ \
if (!r || !cmpfn(code, r->code)) \
return NULL; \
/* MISS, the data is not valid (not present) */ \
if (!CACHE_BTEST(cache->present, r->data)) \
return NULL; \
/* HIT, set as recently used */ \
CACHE_BSET(cache->used, r->data); \
return (&cache->array[r->data]); \
} \
\
\
/* Look for a free spot in the present bitmap and return its index */ \
/* If there is no free space left then just return the first position */ \
int name##_get_free_spot(struct name *cache) \
{ \
if (!cache) return -1; \
int x = 0; \
for (int b = 0; b < CACHE_BSIZE; b++) { \
if (cache->present[b] == 0) x = 64; \
else x = __builtin_clzll(cache->present[b]); \
x = 64-x; \
if (!CACHE_BTEST(cache->present, x+64*b)) \
return x+64*b; \
} \
return 0; \
} \
\
\
const type * name##_insert_at(struct name *cache, const type *g, uint32_t code, int x)\
{ \
if (!cache) return NULL; \
type *spot = NULL; \
/* check if the spot is valid */ \
if (x < 0) return NULL; \
/* Set used and present */ \
CACHE_BSET(cache->present, x); \
CACHE_BSET(cache->used, x); \
CACHE_CYCLE(cache) \
spot = &(cache->array[x]); \
*spot = *g; \
struct name##table_entry e = { .code = code, .data = x}; \
if (!name##table_insert(cache->table, &e)) \
return NULL; \
return spot; \
} \
\
\
const type * name##_insert(struct name *cache, const type *g, uint32_t code, int *x)\
{ \
int y; \
if (!x) x = &y; \
*x = name##_get_free_spot(cache); \
return name##_insert_at(cache, g, code, *x); \
} \
#endif

View File

@ -1,134 +0,0 @@
#ifndef _HASH_GENERIC
#define _HASH_GENERIC
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#define HASH_MAXSIZE 4096
// for fibonacci hashing, 2^{32,64}/<golden ratio>
#define HASH_RATIO32 ((uint64_t)2654435769u)
#define HASH_RATIO64 ((uint64_t)11400714819322457583u)
// salt for string hashing
#define HASH_SALT ((uint64_t)0xbabb0cac)
/* Ready-made compares */
static inline int hash_cmp_u32(uint32_t a, uint32_t b) { return a == b; }
static inline int hash_cmp_u64(uint64_t a, uint64_t b) { return a == b; }
static inline int hash_cmp_str(const char *a, const char *b) { return strcmp(a, b) == 0; }
/* Ready-made hashes */
static inline uint32_t hash_u64(uint64_t c)
{
return (uint64_t)((((uint64_t)c+HASH_SALT)*HASH_RATIO64)>>32);
}
static inline uint32_t hash_u32(uint32_t c)
{
return (uint32_t)((((uint64_t)c<<31)*HASH_RATIO64)>>32);
}
static inline uint32_t hash_str(const char *s)
{
uint32_t h = HASH_SALT;
const uint8_t *v = (const uint8_t *)(s);
for (int x = *s; x; x--) {
h += v[x-1];
h += h << 10;
h ^= h >> 6;
}
h += h << 3;
h ^= h >> 11;
h += h << 15;
return h;
}
#define HASH_DECL(htname, codetype, datatype, hashfn, cmpfn) \
struct htname##_entry { \
codetype code; \
datatype data; \
}; \
\
struct htname##_ref { \
uint32_t items, size, exp; \
struct htname##_entry bucket[]; \
}; \
\
\
struct htname##_ref * htname##_create(uint32_t size) \
{ \
if (!size || size > HASH_MAXSIZE) \
return NULL; \
/* round to the greater power of two */ \
/* FIXME: check for intger overflow here */ \
uint32_t exp = 32-__builtin_clz(size-1); \
size = 1<<(exp); \
/* FIXME: check for intger overflow here */ \
struct htname##_ref *ht = malloc(sizeof(struct htname##_ref)+sizeof(struct htname##_entry)*size); \
if (ht) { \
ht->items = 0; \
ht->size = size; \
ht->exp = exp; \
memset(ht->bucket, 0, sizeof(struct htname##_entry)*size); \
} \
return ht; \
} \
\
\
void htname##_destroy(struct htname##_ref *ht) \
{ \
if (ht) free(ht); \
} \
\
\
static uint32_t htname##_lookup(struct htname##_ref *ht, uint32_t hash, uint32_t idx) \
{ \
if (!ht) return 0; \
uint32_t mask = ht->size-1; \
uint32_t step = (hash >> (32 - ht->exp)) | 1; \
return (idx + step) & mask; \
} \
\
\
/* Find and return the element by code */ \
struct htname##_entry * htname##_search(struct htname##_ref *ht, codetype code)\
{ \
if (!ht) return NULL; \
uint32_t h = hashfn(code); \
for (uint32_t i=h, x=0; ; x++) { \
i = htname##_lookup(ht, h, i); \
if (x > (ht->size<<1) || \
!ht->bucket[i].code || \
cmpfn(ht->bucket[i].code, code) \
) { \
return &(ht->bucket[i]); \
} \
} \
return NULL; \
} \
\
\
/* FIXME: this simply overrides the found item */ \
struct htname##_entry * htname##_insert(struct htname##_ref *ht, struct htname##_entry *entry) \
{ \
struct htname##_entry *r = htname##_search(ht, entry->code); \
if (r) { \
if (!r->code) \
ht->items++; \
*r = *entry; \
} \
return r; \
} \
\
\
struct htname##_entry * htname##_remove(struct htname##_ref *ht, codetype code)\
{ \
if (!ht) return NULL; \
struct htname##_entry *r = htname##_search(ht, code); \
if (r) r->code = 0; \
return r; \
} \
#endif

View File

@ -1,118 +0,0 @@
#ifndef _STACK_GENERIC_H
#define _STACK_GENERIC_H
#define STACK_STEP 8
#define STACK_SALT 0xbabb0cac
// FIXME: find a way to not re-hash the whole stack when removing one item
// incremental hash for every grow
#if STACK_DISABLE_HASH
#define STACK_HASH(p, s, h) {}
#else
#define STACK_HASH(p, s, h) \
{ \
unsigned char *v = (unsigned char *)(p); \
for (int x = (s); x; x--) { \
(h) += v[x-1]; \
(h) += (h) << 10; \
(h) ^= (h) >> 6; \
} \
(h) += (h) << 3; \
(h) ^= (h) >> 11; \
(h) += (h) << 15; \
}
#endif
// TODO: add a rolling hash
#define STACK_DECL(stackname, type) \
struct stackname { \
type *items; \
int size, idx, old_idx; \
unsigned int hash, old_hash; \
}; \
\
\
struct stackname stackname##_init(void) \
{ \
return (struct stackname){0, .hash = STACK_SALT}; \
} \
\
\
int stackname##_grow(struct stackname *stack, int step) \
{ \
if (!stack) \
return -1; \
stack->items = realloc(stack->items, (stack->size+step)*sizeof(type)); \
if(!stack->items) \
return -1; \
memset(&(stack->items[stack->size]), 0, step*sizeof(*(stack->items))); \
stack->size += step; \
return 0; \
} \
\
\
int stackname##_push(struct stackname *stack, type *e) \
{ \
if (!stack || !e) \
return -1; \
if (stack->idx >= stack->size) \
if (stackname##_grow(stack, STACK_STEP)) \
return -1; \
stack->items[stack->idx++] = *e; \
STACK_HASH(e, sizeof(type), stack->hash); \
return 0; \
} \
\
\
type stackname##_pop(struct stackname *stack) \
{ \
if (!stack || stack->idx == 0 || stack->size == 0) \
return (type){0}; \
stack->hash = STACK_SALT; \
STACK_HASH(stack->items, sizeof(type)*(stack->idx-1), stack->hash); \
return stack->items[stack->idx--]; \
} \
\
\
int stackname##_clear(struct stackname *stack) \
{ \
if (!stack) \
return -1; \
stack->old_idx = stack->idx; \
stack->old_hash = stack->hash; \
stack->hash = STACK_SALT; \
stack->idx = 0; \
return 0; \
} \
\
\
int stackname##_changed(struct stackname *stack) \
{ \
if (!stack) \
return -1; \
return stack->hash != stack->old_hash; \
} \
\
\
int stackname##_size_changed(struct stackname *stack) \
{ \
if (!stack) \
return -1; \
return stack->size != stack->old_idx; \
} \
\
\
int stackname##_free(struct stackname *stack) \
{ \
if (stack) { \
stackname##_clear(stack); \
if (stack->items) \
free(stack->items); \
} \
return 0; \
} \
#endif

View File

@ -1,46 +0,0 @@
#!/bin/sh
genobj() {
#echo "$1" | sed -e 's/^/obj\//' -e 's/\.c/\.o/'
echo "$1" | sed -e 's/\.c/\.o/'
}
genrule() {
#gcc -MM "$1" | sed 's/^/obj\//'
gcc -MM "$1"
}
rm -f objlist
cat > Makefile << EOF
CC = gcc
# instrumentation flags
INFLAGS = -fsanitize=address,builtin,undefined
LDFLAGS = -lm -lgrapheme -lSDL2 -lGLEW -lGL \${INFLAGS}
CFLAGS = -ggdb3 -Wall -Wextra -pedantic -std=c11 \
-Wno-unused-function -fno-omit-frame-pointer \${INFLAGS}
.PHONY: clean all
all: test
EOF
for f in *.c; do
genrule $f >> Makefile
genobj $f >> objlist
done
mainrule='test: '
linkcmd=' ${CC} ${LDFLAGS} -o test '
cleanrule='clean:
rm -f test '
while IFS="" read -r line; do
mainrule="$mainrule $line"
linkcmd="$linkcmd $line"
cleanrule="$cleanrule $line"
done < objlist
echo "$mainrule" >> Makefile
echo "$linkcmd" >> Makefile
echo "$cleanrule" >> Makefile

View File

@ -1,96 +0,0 @@
#include <SDL2/SDL.h>
#include <SDL2/SDL_events.h>
#include <SDL2/SDL_video.h>
#include <stdlib.h>
#include <locale.h>
#include <stdio.h>
#include "ren.h"
#include "util.h"
//const char *str1 = "Ciao Mamma!\nprova: òçà°ù§|¬³¼$£ì\t";
const char *str1 = "ciao\tmamma";
const char *str2 = "The quick brown fox jumps over the lazy dog\n"
"чащах юга жил бы цитрус? Да, но фальшивый экземпляр!\n"
"Pijamalı hasta, yağız şoföre çabucak güvendi\n"
"Pchnąć w tę łódź jeża lub ośm skrzyń fig\n"
"イロハニホヘト チリヌルヲ ワカヨタレソ ツネナラム ウヰノオクヤマ ケフコエテ アサキユメミシ ヱヒモセスン\n"
"Kæmi ný öxi hér ykist þjófum nú bæði víl og ádrepa";
SDL_Window *win;
#define red 0xff0000ff
#define blue 0xffff0000
void draw(void)
{
static unsigned int frame = 0;
printf("frame: %d\n", frame++);
ren_clear();
//ren_render_box(10, 10, 400, 50, blue);
//if (ren_render_text(str1, 10, 10, 400, 50, 20))
// printf("text: %s\n", ren_strerror());
int w, h;
ren_get_text_box(str2, &w, &h, 20);
//printf("box for: %s -> (%d, %d)\n", str2, w, h);
ren_render_box(0, 0, w, h, blue);
ren_render_text(str2, 0, 0, w, h, 20);
// fixme: this causes a bug
const char *s = "ciao mamma";
ren_get_text_box(s, &w, &h, 12);
s = "stuff that was not in font size 12 -> чащаx";
ren_render_box(0, 200, w, h, red);
if (ren_render_text(s, 0, 200, 0xffff, 0xffff, 12))
printf("BUG\n");
SDL_GL_SwapWindow(win);
}
int main(void)
{
setlocale(LC_ALL, "en_US.UTF-8");
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0");
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
SDL_SetHint(SDL_HINT_VIDEO_HIGHDPI_DISABLED, "0");
win = SDL_CreateWindow(
"test render",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
500,
500,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
if (ren_init(win)) {
printf("renderer init error: %s\n", ren_strerror());
return 1;
}
SDL_Event e;
while(1) {
SDL_WaitEvent(&e);
if (e.type == SDL_QUIT)
break;
if (e.type == SDL_WINDOWEVENT) {
switch (e.window.event) {
case SDL_WINDOWEVENT_RESIZED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
ren_update_viewport(e.window.data1, e.window.data2);
draw();
break;
case SDL_WINDOWEVENT_EXPOSED:
draw();
break;
default: break;
}
}
}
ren_free();
SDL_DestroyWindow(win);
SDL_Quit();
return 0;
}

View File

@ -1,826 +0,0 @@
#include <SDL2/SDL.h>
#include <GL/glew.h>
#include <SDL2/SDL_opengl.h>
#include <SDL2/SDL_video.h>
#include <ctype.h>
#include <grapheme.h>
#include <stdio.h>
#include "util.h"
#include "font.h"
#include "ren.h"
#define GLERR(x) \
{ \
int a = glGetError(); \
if (a != GL_NO_ERROR) \
printf("(%s:%d %s:%s) glError: 0x%x %s\n", \
__FILE__, __LINE__, __func__, x, a, glerr[a&0xff]); \
}
#define GL(f) f; GLERR(#f)
#define REN_RET(a,b) {ren_errno = b; return a;}
enum REN_ERR {
REN_SUCCESS = 0,
REN_ERRNO,
REN_INVAL,
REN_VERTEX,
REN_FRAGMENT,
REN_PROGRAM,
REN_COMPILE,
REN_LINK,
REN_TEXTURE,
REN_CONTEXT,
REN_GLEW,
REN_FONT,
REN_BUFFER,
REN_UNIFORM,
};
// TODO: make a macro for enum-associated string arrays
const char * ren_err_msg[] = {
[REN_SUCCESS] = "Success",
[REN_ERRNO] = "Look at errno",
[REN_INVAL] = "Invalid or NULL arguments",
[REN_VERTEX] = "Failed to create opengl vertex shader",
[REN_FRAGMENT] = "Failed to create opengl fragment shader",
[REN_PROGRAM] = "Failed to create opengl program",
[REN_COMPILE] = "Failed to compile shaders",
[REN_LINK] = "Failed to link shaders",
[REN_TEXTURE] = "Failed to create texture",
[REN_CONTEXT] = "Failed to create SDL OpenGL context",
[REN_GLEW] = "GLEW Error",
[REN_FONT] = "Font Error",
[REN_BUFFER] = "Failed to create opengl buffer",
[REN_UNIFORM] = "Failed to get uniform location",
};
#define ELEM(x) [x&0xff] = #x,
const char *glerr[] = {
ELEM(GL_INVALID_ENUM)
ELEM(GL_INVALID_VALUE)
ELEM(GL_INVALID_OPERATION)
ELEM(GL_OUT_OF_MEMORY)
};
#undef ELEM
// different stacks
#include "generic_stack.h"
STACK_DECL(vtstack, struct v_text)
STACK_DECL(vcstack, struct v_col)
struct ren_font {
struct font_atlas *font;
GLuint texture;
};
struct {
SDL_GLContext *gl;
struct ren_font *fonts;
int fonts_no;
GLuint font_prog;
GLuint box_prog;
GLuint font_buffer;
GLuint box_buffer;
GLint viewsize_loc;
GLint box_viewsize_loc;
GLint texturesize_loc;
struct vtstack font_stack;
struct vcstack box_stack;
int width, height;
int s_x, s_y, s_w, s_h;
int tabsize;
} ren = {0};
static int ren_errno;
// print shader compilation errors
static int shader_compile_error(GLuint shader, const char *path)
{
GLint status;
GL(glGetShaderiv(shader, GL_COMPILE_STATUS, &status))
if (status != GL_FALSE)
return 0;
GLint log_length;
GL(glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_length))
GLchar *log_str = emalloc((log_length + 1)*sizeof(GLchar));
GL(glGetShaderInfoLog(shader, log_length, NULL, log_str))
const char *shader_type_str = NULL;
GLint shader_type;
GL(glGetShaderiv(shader, GL_SHADER_TYPE, &shader_type))
switch(shader_type) {
case GL_VERTEX_SHADER: shader_type_str = "vertex"; break;
case GL_GEOMETRY_SHADER: shader_type_str = "geometry"; break;
case GL_FRAGMENT_SHADER: shader_type_str = "fragment"; break;
}
fprintf(stderr, "Compile failure in %s shader %s:\n%s\n", shader_type_str, path, log_str);
efree(log_str);
return -1;
}
// print shader link errors
static int shader_link_error(GLuint prog)
{
GLint status;
GL(glGetProgramiv(prog, GL_LINK_STATUS, &status))
if (status != GL_FALSE)
return 0;
GLint log_length;
GL(glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &log_length))
GLchar *log_str = emalloc((log_length + 1)*sizeof(GLchar));
GL(glGetProgramInfoLog(prog, log_length, NULL, log_str))
fprintf(stderr, "Linker failure: %s\n", log_str);
efree(log_str);
return -1;
}
static GLuint shader_compile(const char *path, const char *shader, GLuint type)
{
GLuint s;
// initialize the vertex shader and get the corresponding id
s = GL(glCreateShader(type))
if (!s) REN_RET(0, REN_VERTEX)
// get the shader into opengl
GL(glShaderSource(s, 1, &shader, NULL))
GL(glCompileShader(s))
if (shader_compile_error(s, path))
REN_RET(0, REN_COMPILE)
return s;
}
const char * ren_strerror(void)
{
return ren_err_msg[ren_errno % (sizeof(ren_err_msg)/sizeof(char *))];
}
// compile a vertex shader (vs_path) and a fragment shader (fs_path) into an opengl
// program and return it's index
static GLuint ren_compile_program(const char *vs_path, const char *fs_path)
{
GLuint gl_vertshader, gl_fragshader, prog;
if (!vs_path || !fs_path)
REN_RET(0, REN_INVAL)
char *str = NULL;
dump_file(vs_path, &str, NULL);
if (!str) REN_RET(0, REN_ERRNO)
gl_vertshader = shader_compile(vs_path, str, GL_VERTEX_SHADER);
efree(str);
if (!gl_vertshader)
return 0;
dump_file(fs_path, &str, NULL);
if (!str) REN_RET(0, REN_ERRNO)
gl_fragshader = shader_compile(fs_path, str, GL_FRAGMENT_SHADER);
efree(str);
if (!gl_fragshader)
return 0;
// create the main program object, it is an amalgamation of all shaders
prog = GL(glCreateProgram())
if (!prog) REN_RET(0, REN_PROGRAM)
// attach the shaders to the program (set which shaders are present)
GL(glAttachShader(prog, gl_vertshader))
GL(glAttachShader(prog, gl_fragshader))
// then link the program (basically the linking stage of the program)
GL(glLinkProgram(prog))
if (shader_link_error(prog))
REN_RET(0, REN_LINK)
// after linking the shaders can be detached and the source freed from
// memory since the program is ready to use
GL(glDetachShader(prog, gl_vertshader))
GL(glDetachShader(prog, gl_fragshader))
return prog;
}
static void set_texture_parameters(GLuint type, GLuint wrap_s, GLuint wrap_t, GLuint upscale, GLuint downscale)
{
GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap_s))
GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap_t))
GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, upscale))
GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, downscale))
}
static GLuint ren_texturergb_2d(const char *buf, int w, int h, int upscale, int downscale)
{
GLuint t;
if (!buf || w <= 0 || h <= 0)
REN_RET(0, REN_INVAL)
GL(glGenTextures(1, &t))
if (!t) REN_RET(0, REN_TEXTURE)
GL(glBindTexture(GL_TEXTURE_2D, t))
GL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, buf))
set_texture_parameters(GL_TEXTURE_2D, GL_REPEAT, GL_REPEAT, upscale, downscale);
return t;
}
static GLuint ren_texturergba_2d(const char *buf, int w, int h, int upscale, int downscale)
{
GLuint t;
if (!buf || w <= 0 || h <= 0)
REN_RET(0, REN_INVAL)
GL(glGenTextures(1, &t))
if (!t) REN_RET(0, REN_TEXTURE)
GL(glBindTexture(GL_TEXTURE_2D, t))
GL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, buf))
set_texture_parameters(GL_TEXTURE_2D, GL_REPEAT, GL_REPEAT, upscale, downscale);
return t;
}
static GLuint ren_texturer_2d(const char *buf, int w, int h, int upscale, int downscale)
{
GLuint t;
if (!buf || w <= 0 || h <= 0)
REN_RET(0, REN_INVAL)
GL(glGenTextures(1, &t))
if (!t) REN_RET(0, REN_TEXTURE)
GL(glBindTexture(GL_TEXTURE_2D, t))
GL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, buf))
set_texture_parameters(GL_TEXTURE_2D, GL_REPEAT, GL_REPEAT, upscale, downscale);
return t;
}
static GLuint ren_texturer_rect(const char *buf, int w, int h, int upscale, int downscale)
{
GLuint t;
if (!buf || w <= 0 || h <= 0)
REN_RET(0, REN_INVAL)
GL(glGenTextures(1, &t))
if (!t) REN_RET(0, REN_TEXTURE)
GL(glBindTexture(GL_TEXTURE_RECTANGLE, t))
GL(glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_RED, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, buf))
// a limitation of recatngle textures is that the wrapping mode is limited
// to either clamp-to-edge or clamp-to-border
set_texture_parameters(GL_TEXTURE_RECTANGLE, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, upscale, downscale);
return t;
}
// FIXME: update only the newly generated character instead of the whole texture
static int update_font_texture(int idx)
{
GL(glUseProgram(ren.font_prog))
GL(glTexSubImage2D(
GL_TEXTURE_RECTANGLE,
0, 0, 0,
ren.fonts[idx].font->width,
ren.fonts[idx].font->height,
GL_RED,
GL_UNSIGNED_BYTE,
ren.fonts[idx].font->atlas))
//font_dump(ren.fonts[idx].font, "./atlas.png");
GL(glUseProgram(0));
return 0;
}
// loads a font and returns it's index in the fonts array
static int ren_load_font(int size, const char *path)
{
int idx = ren.fonts_no;
struct font_atlas *f;
ren.fonts = erealloc(ren.fonts, sizeof(struct ren_font)*(idx+1));
ren.fonts[idx].font = font_init();
f = ren.fonts[idx].font;
if (!f)
REN_RET(-1, REN_FONT)
if (!path)
path = DEFAULT_FONT;
if (font_load(f, path, size))
REN_RET(-1, REN_FONT)
//font_dump(f, "./atlas.png");
// load font texture (atlas)
ren.fonts[idx].texture = ren_texturer_rect(
(const char *)f->atlas,
f->width,
f->height,
GL_LINEAR, GL_LINEAR);
if (!ren.fonts[idx].texture)
return -1;
ren.fonts_no = idx+1;
return idx;
}
// returns the index to the ren.fonts array to the font with the correct size
// return -1 on errror
static int ren_get_font(int size)
{
for (int i = 0; i < ren.fonts_no; i++) {
if (ren.fonts[i].font->size == size)
return i;
}
// TODO: add a way to change font family
return ren_load_font(size, NULL);
}
int ren_init(SDL_Window *w)
{
// Initialize OpenGL
if (!w)
REN_RET(-1, REN_INVAL)
// using version 3 does not allow to use glVertexAttribPointer() without
// vertex buffer objects, so use compatibility mode
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
ren.gl = SDL_GL_CreateContext(w);
if (!ren.gl) {
printf("SDL: %s\n", SDL_GetError());
REN_RET(-1, REN_CONTEXT)
}
GL(glEnable(GL_BLEND))
GL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA))
GL(glDisable(GL_CULL_FACE))
GL(glDisable(GL_DEPTH_TEST))
GL(glEnable(GL_SCISSOR_TEST))
GL(glEnable(GL_TEXTURE_2D))
GL(glEnable(GL_TEXTURE_RECTANGLE))
GLenum glew_err = glewInit();
if (glew_err != GLEW_OK)
REN_RET(glew_err, REN_GLEW);
// Create stacks
ren.font_stack = vtstack_init();
ren.box_stack = vcstack_init();
// generate the font buffer object
GL(glGenBuffers(1, &ren.font_buffer))
if (!ren.font_buffer) REN_RET(-1, REN_BUFFER)
GL(glGenBuffers(1, &ren.box_buffer))
if (!ren.box_buffer) REN_RET(-1, REN_BUFFER)
// Compile font shaders
ren.font_prog = ren_compile_program(FONT_VERSHADER, FONT_FRAGSHADER);
if (!ren.font_prog) return -1;
// create the uniforms, if the returned value is -1 then the uniform may have
// been optimized away, in any case do not return, just give a warning
ren.viewsize_loc = GL(glGetUniformLocation(ren.font_prog, "viewsize"))
if (ren.viewsize_loc == -1)
printf("uniform %s was optimized away\n", "viewsize");
ren.texturesize_loc = GL(glGetUniformLocation(ren.font_prog, "texturesize"))
if (ren.texturesize_loc == -1)
printf("uniform %s was optimized away\n", "texturesize");
// Compile box shaders
ren.box_prog = ren_compile_program(BOX_VERSHADER, BOX_FRAGSHADER);
if (!ren.box_prog) return -1;
ren.box_viewsize_loc = GL(glGetUniformLocation(ren.box_prog, "viewsize"))
if (ren.box_viewsize_loc == -1)
printf("uniform %s was optimized away\n", "viewsize");
// Finishing touches
ren.tabsize = REN_TABSIZE;
int width, height;
SDL_GetWindowSize(w, &width, &height);
ren_update_viewport(width, height);
GL(glClearColor(0.3f, 0.3f, 0.3f, 0.f))
GL(glClear(GL_COLOR_BUFFER_BIT))
return 0;
}
int ren_clear(void)
{
GL(glScissor(0, 0, ren.width, ren.height))
GL(glClear(GL_COLOR_BUFFER_BIT));
return 0;
}
// idx refers to the fonts array index, this means that when drawing the font stack
// only one font size at the time can be used
static int ren_draw_font_stack(int idx)
{
struct font_atlas *font = ren.fonts[idx].font;
GLuint font_texture = ren.fonts[idx].texture;
GL(glUseProgram(ren.font_prog))
GL(glBindTexture(GL_TEXTURE_RECTANGLE, font_texture))
GL(glViewport(0, 0, ren.width, ren.height))
// this has caused me some trouble, convert from image coordiates to viewport
GL(glScissor(ren.s_x, ren.height-ren.s_y-ren.s_h, ren.s_w, ren.s_h))
GL(glUniform2i(ren.viewsize_loc, ren.width, ren.height))
GL(glUniform2i(ren.texturesize_loc, font->width, font->height))
GL(glBindBuffer(GL_ARRAY_BUFFER, ren.font_buffer))
if (vtstack_changed(&ren.font_stack)) {
if (vtstack_size_changed(&ren.font_stack)) {
GL(glBufferData(
GL_ARRAY_BUFFER,
ren.font_stack.idx*sizeof(struct v_text),
ren.font_stack.items,
GL_DYNAMIC_DRAW))
} else {
GL(glBufferSubData(
GL_ARRAY_BUFFER,
0,
ren.font_stack.idx*sizeof(struct v_text),
ren.font_stack.items))
}
}
// when passing ints to glVertexAttribPointer they are automatically
// converted to floats
GL(glVertexAttribPointer(
REN_VERTEX_IDX,
2,
GL_INT,
GL_FALSE,
sizeof(struct v_text),
0))
GL(glVertexAttribPointer(
REN_UV_IDX,
2,
GL_INT,
GL_FALSE,
sizeof(struct v_text),
(void*)sizeof(vec2_i)))
GL(glEnableVertexAttribArray(REN_VERTEX_IDX))
GL(glEnableVertexAttribArray(REN_UV_IDX))
GL(glDrawArrays(GL_TRIANGLES, 0, ren.font_stack.idx))
GL(glDisableVertexAttribArray(REN_VERTEX_IDX))
GL(glDisableVertexAttribArray(REN_UV_IDX))
GL(glBindBuffer(GL_ARRAY_BUFFER, 0))
GL(glBindTexture(GL_TEXTURE_RECTANGLE, 0))
GL(glUseProgram(0))
vtstack_clear(&ren.font_stack);
return 0;
}
static int ren_draw_box_stack(void)
{
GL(glUseProgram(ren.box_prog))
GL(glViewport(0, 0, ren.width, ren.height))
GL(glScissor(0, 0, ren.width, ren.height))
GL(glUniform2i(ren.box_viewsize_loc, ren.width, ren.height))
GL(glBindBuffer(GL_ARRAY_BUFFER, ren.box_buffer))
if(vcstack_changed(&ren.box_stack)) {
if (vcstack_size_changed(&ren.box_stack)) {
GL(glBufferData(
GL_ARRAY_BUFFER,
ren.box_stack.idx*sizeof(struct v_col),
ren.box_stack.items,
GL_DYNAMIC_DRAW))
} else {
GL(glBufferSubData(
GL_ARRAY_BUFFER,
0,
ren.box_stack.idx*sizeof(struct v_col),
ren.box_stack.items))
}
}
// when passing ints to glVertexAttribPointer they are automatically
// converted to floats
GL(glVertexAttribPointer(
REN_VERTEX_IDX,
2,
GL_INT,
GL_FALSE,
sizeof(struct v_col),
0))
// the color gets normalized
GL(glVertexAttribPointer(
REN_COLOR_IDX,
4,
GL_INT,
GL_TRUE,
sizeof(struct v_col),
(void*)sizeof(vec2_i)))
GL(glEnableVertexAttribArray(REN_VERTEX_IDX))
GL(glEnableVertexAttribArray(REN_COLOR_IDX))
GL(glDrawArrays(GL_TRIANGLES, 0, ren.box_stack.idx))
GL(glDisableVertexAttribArray(REN_VERTEX_IDX))
GL(glDisableVertexAttribArray(REN_COLOR_IDX))
GL(glBindBuffer(GL_ARRAY_BUFFER, 0))
GL(glUseProgram(0))
vcstack_clear(&ren.box_stack);
return 0;
}
int ren_update_viewport(int w, int h)
{
ren.width = w;
ren.height = h;
return 0;
}
int ren_set_scissor(int x, int y, int w, int h)
{
ren.s_x = x;
ren.s_y = y;
ren.s_w = w;
ren.s_h = h;
return 0;
}
static int ren_push_glyph(const struct font_glyph *g, int gx, int gy)
{
/* x4,y4 x3,y3
* o-------------+
* |(x,y) /|
* | / |
* | 2 / |
* | / |
* | / |
* | / 1 |
* |/ |
* +-------------+
* x1,y1 x2,y2 */
struct v_text v;
struct font_glyph c;
c = *g;
//printf("g: u=%d v=%d w=%d h=%d a=%d x=%d y=%d\n", c.u, c.v, c.w, c.h, c.a, c.x, c.y);
//printf("v: x=%d y=%d u=%d v=%d\n", v.pos.x, v.pos.y, v.uv.u, v.uv.v);
// x1,y1
v = (struct v_text){
.pos = { .x = gx+c.x, .y = gy+c.y+c.h },
.uv = { .u = c.u, .v = c.v+c.h },
};
vtstack_push(&ren.font_stack, &v);
// x2,y2
v = (struct v_text){
.pos = { .x = gx+c.x+c.w, .y = gy+c.y+c.h },
.uv = { .u = c.u+c.w, .v = c.v+c.h },
};
vtstack_push(&ren.font_stack, &v);
// x3,y3
v = (struct v_text){
.pos = { .x = gx+c.x+c.w, .y = gy+c.y },
.uv = { .u = c.u+c.w, .v = c.v },
};
vtstack_push(&ren.font_stack, &v);
// x1,y1
v = (struct v_text){
.pos = { .x = gx+c.x, .y = gy+c.y+c.h },
.uv = { .u = c.u, .v = c.v+c.h },
};
vtstack_push(&ren.font_stack, &v);
// x3,y3
v = (struct v_text){
.pos = { .x = gx+c.x+c.w, .y = gy+c.y },
.uv = { .u = c.u+c.w, .v = c.v },
};
vtstack_push(&ren.font_stack, &v);
// x4,y4
v = (struct v_text){
.pos = { .x = gx+c.x, .y = gy+c.y },
.uv = { .u = c.u, .v = c.v },
};
vtstack_push(&ren.font_stack, &v);
return 0;
}
static const struct font_glyph * get_glyph(unsigned int code, int idx)
{
const struct font_glyph *g;
int updated;
g = font_get_glyph_texture(ren.fonts[idx].font, code, &updated);
if (!g)
REN_RET(NULL, REN_FONT);
if (updated) {
if (update_font_texture(idx))
REN_RET(NULL, REN_TEXTURE);
}
return g;
}
// TODO: reduce repeating patterns in ren_get_text_box() and ren_render_text()
int ren_get_text_box(const char *str, int *rw, int *rh, int size)
{
int w = 0, h = 0, x = 0, y = 0;
const struct font_glyph *g;
size_t off, ret;
uint32_t cp;
int idx = ren_get_font(size);
if (idx < 0)
return -1;
h = y = ren.fonts[idx].font->glyph_max_h;
for (off = 0; (ret = grapheme_decode_utf8(str+off, SIZE_MAX, &cp)) > 0 && cp != 0; off += ret) {
if (iscntrl(cp)) goto skip_get;
if (!(g = get_glyph(cp, idx)))
return -1;
x += g->x + g->a;
// FIXME: generalize this thing
skip_get:
switch (cp) {
case '\t': {
const struct font_glyph *sp = get_glyph(' ', idx);
if (!sp) return -1;
x += (sp->x + sp->a)*ren.tabsize;
}
break;
case '\r':
x = 0;
break;
case '\n':
// TODO: encode and/or store line height
y += ren.fonts[idx].font->glyph_max_h;
x = 0;
break;
default: break;
}
if (x > w) w = x;
if (y > h) h = y;
}
if (rw) *rw = w;
if (rh) *rh = h;
return 0;
}
int ren_render_text(const char *str, int x, int y, int w, int h, int size)
{
const struct font_glyph *g;
size_t ret, off;
uint32_t cp;
int gx = x, gy = y;
int idx = ren_get_font(size);
if (idx < 0)
return -1;
for (off = 0; (ret = grapheme_decode_utf8(str+off, SIZE_MAX, &cp)) > 0 && cp != 0; off += ret) {
// skip special characters that render a box (not present in font)
if (iscntrl(cp)) goto skip_render;
if (!(g = get_glyph(cp, idx)))
return -1;
// only push the glyph if it is inside the bounding box
if (gx <= x+w && gy <= y+h)
ren_push_glyph(g, gx, gy);
// TODO: possible kerning needs to be applied here
// TODO: handle other unicode control characters such as the
// right-to-left isolate (\u2067)
gx += g->x + g->a;
skip_render:
switch (cp) {
case '\t': {
const struct font_glyph *sp = get_glyph(' ', idx);
if (!sp) return -1;
gx += (sp->x + sp->a)*ren.tabsize;
}
break;
case '\r':
gx = x;
break;
case '\n':
gy += ren.fonts[idx].font->glyph_max_h;
gx = x;
break;
default: break;
}
}
ren_set_scissor(x, y, w, h);
ren_draw_font_stack(idx);
return 0;
}
// re-normalize color from 0-255 to 0-0x7fffffff, technically i'm dividing by 256 here
#define RENORM(x) (((unsigned long long)(x)*0x7fffffff)>>8)
#define R(x) (x&0xff)
#define G(x) ((x>>8)&0xff)
#define B(x) ((x>>16)&0xff)
#define A(x) ((x>>24)&0xff)
static int ren_push_box(int x, int y, int w, int h, unsigned int color)
{
/* x4,y4 x3,y3
* o-------------+
* |(x,y) /|
* | / |
* | 2 / |
* | / |
* | / |
* | / 1 |
* |/ |
* +-------------+
* x1,y1 x2,y2 */
struct v_col v;
vec4_i c = {
.r = RENORM(R(color)),
.g = RENORM(G(color)),
.b = RENORM(B(color)),
.a = RENORM(A(color)),
};
// x1, y1
v = (struct v_col){ .pos = { .x = x, .y = y+h }, .col = c };
vcstack_push(&ren.box_stack, &v);
// x2, y2
v = (struct v_col){ .pos = { .x = x+w, .y = y+h }, .col = c };
vcstack_push(&ren.box_stack, &v);
// x3, y3
v = (struct v_col){ .pos = { .x = x+w, .y = y }, .col = c };
vcstack_push(&ren.box_stack, &v);
// x1, y1
v = (struct v_col){ .pos = { .x = x, .y = y+h }, .col = c };
vcstack_push(&ren.box_stack, &v);
// x3, y3
v = (struct v_col){ .pos = { .x = x+w, .y = y }, .col = c };
vcstack_push(&ren.box_stack, &v);
// x4, y4
v = (struct v_col){ .pos = { .x = x, .y = y }, .col = c };
vcstack_push(&ren.box_stack, &v);
return 0;
}
int ren_render_box(int x, int y, int w, int h, unsigned int color)
{
ren_push_box(x, y, w, h, color);
ren_draw_box_stack();
return 0;
}
int ren_free(void)
{
GL(glUseProgram(0))
GL(glBindBuffer(GL_ARRAY_BUFFER, 0))
GL(glDeleteProgram(ren.box_prog));
GL(glDeleteProgram(ren.font_prog));
GL(glDeleteBuffers(1, &ren.font_buffer))
for (int i = 0; i < ren.fonts_no; i++) {
GL(glDeleteTextures(1, &ren.fonts[i].texture))
font_free(ren.fonts[i].font);
}
SDL_GL_DeleteContext(ren.gl);
vtstack_free(&ren.font_stack);
vcstack_free(&ren.box_stack);
return 0;
}

View File

@ -1,54 +0,0 @@
#ifndef _RENDERER_H
#define _RENDERER_H
#include <SDL2/SDL.h>
#define DEFAULT_FONT "/usr/share/fonts/TTF/FiraCode-Regular.ttf"
#define FONT_VERSHADER "./font_vertshader.glsl"
#define FONT_FRAGSHADER "./font_fragshader.glsl"
#define BOX_VERSHADER "./box_vertshader.glsl"
#define BOX_FRAGSHADER "./box_fragshader.glsl"
#define REN_VERTEX_IDX 0
#define REN_UV_IDX 1
#define REN_COLOR_IDX 2
#define REN_TABSIZE 8
typedef struct {
union { int x, u; };
union { int y, v; };
} vec2_i;
typedef struct {
union { int x, r; };
union { int y, g; };
union { int z, b; };
union { int w, a; };
} vec4_i;
// textured vertex
struct v_text {
vec2_i pos;
vec2_i uv;
};
// colored vertex
struct v_col {
vec2_i pos;
vec4_i col;
};
int ren_init(SDL_Window *sdl_window);
int ren_free(void);
const char * ren_strerror(void);
int ren_update_viewport(int w, int h);
int ren_set_scissor(int x, int y, int w, int h);
int ren_get_text_box(const char *str, int *rw, int *rh, int size);
int ren_render_text(const char *str, int x, int y, int w, int h, int size);
int ren_render_box(int x, int y, int w, int h, unsigned int color);
int ren_clear(void);
#endif

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,126 +0,0 @@
#define _POSIX_C_SOURCE 200809l
#include <sys/mman.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <err.h>
#include <time.h>
#include "util.h"
const char *bit_rep[16] = {
[ 0] = "0000", [ 1] = "0001", [ 2] = "0010", [ 3] = "0011",
[ 4] = "0100", [ 5] = "0101", [ 6] = "0110", [ 7] = "0111",
[ 8] = "1000", [ 9] = "1001", [10] = "1010", [11] = "1011",
[12] = "1100", [13] = "1101", [14] = "1110", [15] = "1111",
};
void * emalloc(unsigned long int size)
{
void *r = malloc(size);
if (!r)
err(EXIT_FAILURE, "malloc() of size %ld", size);
return r;
}
void * ecalloc(unsigned long int nmemb, unsigned long int size)
{
void *r = calloc(nmemb, size);
if (!r)
err(EXIT_FAILURE, "calloc() of size %ld, nmemb %ld", size, nmemb);
return r;
}
void * erealloc(void *ptr, unsigned long int size)
{
void *r = realloc(ptr, size);
if (!r)
err(EXIT_FAILURE, "ralloc() of 0x%lx, to size %ld", (unsigned long)ptr, size);
return r;
}
void efree(void *ptr)
{
if (ptr)
free(ptr);
}
void map_file(const unsigned char **str, int *size, const char *path)
{
if (!path) {
errno = EINVAL;
err(EXIT_FAILURE, "NULL filename");
}
FILE *fp = fopen(path, "r");
if (!fp)
err(EXIT_FAILURE, "Cannot open file %s", path);
*size = lseek(fileno(fp), 0, SEEK_END);
if (*size == (off_t)-1)
err(EXIT_FAILURE, "lseek() failed");
*str = mmap(0, *size, PROT_READ, MAP_PRIVATE, fileno(fp), 0);
if (*str == (void*)-1)
err(EXIT_FAILURE, "mmap() failed");
if (fclose(fp))
err(EXIT_FAILURE, "Error closing file");
}
void dump_file(const char *path, char **buf, int *buf_len)
{
if (!path) {
errno = EINVAL;
err(EXIT_FAILURE, "NULL filename");
}
if (!buf) {
errno = EINVAL;
err(EXIT_FAILURE, "No buffer specified");
}
int m = 0;
if (!buf_len)
buf_len = &m;
FILE *fp = fopen(path, "r");
if (!fp)
err(EXIT_FAILURE, "Cannot open file %s", path);
*buf_len = lseek(fileno(fp), 0, SEEK_END);
rewind(fp);
if (*buf_len == (off_t)-1)
err(EXIT_FAILURE, "lseek() failed");
*buf = emalloc(*buf_len+1);
memset(*buf, 0, *buf_len+1);
int ret = fread(*buf, 1, *buf_len, fp);
if (ret != *buf_len)
err(EXIT_FAILURE, "fread() returned short %s", ferror(fp) ? "stream error" : feof(fp) ? "EOF reached" : "unknown error");
if (fclose(fp))
err(EXIT_FAILURE, "Error closing file");
}
void print_byte(unsigned char byte)
{
printf("%s%s", bit_rep[byte >> 4], bit_rep[byte & 0x0F]);
}
static struct timespec clock_start, clock_stop;
void stopwatch_start(void)
{
clock_gettime(CLOCK_MONOTONIC_COARSE, &clock_start);
}
double stopwatch_get(void)
{
clock_gettime(CLOCK_MONOTONIC_COARSE, &clock_stop);
return (clock_stop.tv_sec-clock_start.tv_sec)+(double)(clock_stop.tv_nsec-clock_start.tv_nsec)/(double)1000000000L;
}

View File

@ -1,34 +0,0 @@
#ifndef _UTIL_H
#define _UTIL_H
#include <stdio.h>
void * emalloc(unsigned long int size);
void * ecalloc(unsigned long int nmemb, unsigned long int size);
void * erealloc(void *ptr, unsigned long int size);
void efree(void *ptr);
void map_file(const unsigned char **str, int *size, const char *path);
void dump_file(const char *path, char **buf, int *buf_len);
void print_byte(unsigned char byte);
void stopwatch_start(void);
double stopwatch_get(void);
#define TIME_SEC(f) \
{ \
stopwatch_start(); \
f; \
printf("\"%s\" took %f seconds", #f, stopwatch_get()); \
}
#define TIME_MS(f) \
{ \
stopwatch_start(); \
f; \
printf("\"%s\" took %f ms", #f, stopwatch_get()*1000.0f); \
}
#endif

1461
ugui.c

File diff suppressed because it is too large Load Diff

285
ugui.h
View File

@ -1,285 +0,0 @@
#ifndef _UG_HEADER
#define _UG_HEADER
#define UG_STACK(T) struct { T *items; int idx, size, sorted; }
#define BIT(n) (1 << n)
#define RGBA_FORMAT(x) { .a=x&0xff, .b=(x>>8)&0xff, .g=(x>>16)&0xff, .r=(x>>24)&0xff }
#define RGB_FORMAT(x) { .a=0xff, .b=x&0xff, .g=(x>>8)&0xff, .r=(x>>16)&0xff }
#define SIZE_PX(x) { .size.i=x, .unit=UG_UNIT_PX }
#define SIZE_MM(x) { .size.f=x, .unit=UG_UNIT_MM }
#define SIZE_PT(x) { .size.f=x, .unit=UG_UNIT_PT }
#define SQUARE(x) .w = x, .h = x
// basic types
typedef unsigned int ug_id_t;
typedef struct { union {int x, w;}; union {int y, h;}; } ug_vec2_t;
typedef struct { unsigned char a, b, g, r; } ug_color_t;
typedef struct { int x, y, w, h; } ug_rect_t;
typedef struct { union {int i; float f;} size; int unit; } ug_size_t;
// div has information about the phisical dimension
typedef struct { ug_size_t x, y, w, h;} ug_div_t;
typedef enum {
UG_UNIT_PX = 0,
UG_UNIT_MM,
UG_UNIT_PT,
} ug_unit_t;
// element type
typedef struct {
ug_id_t id;
unsigned short int type;
unsigned short int flags;
ug_rect_t rect, rca;
const char *name;
union {
struct {
const char *txt;
} btn;
};
} ug_element_t;
enum {
UG_ELEM_BUTTON, // button
UG_ELEM_TXTBTN, // textual button, a button but without frame
UG_ELEM_CHECK, // checkbox
UG_ELEM_RADIO, // radio button
UG_ELEM_TOGGLE, // toggle button
UG_ELEM_LABEL, // simple text
UG_ELEM_UPDOWN, // text with two buttons up and down
UG_ELEM_TEXTINPUT, // text input box
UG_ELEM_TEXTBOX, // text surrounded by a box
UG_ELEM_IMG, // image, icon
UG_ELEM_SPACE, // takes up space
};
enum {
ELEM_CLIPPED = BIT(0),
};
// container type, a container is an entity that contains layouts, a container has
// a haight a width and their maximum values, in essence a container is a rectangular
// area that can be resized and sits somewhere on the drawable region
// the z index of a container is determined by it's position on the stack
typedef struct {
ug_id_t id;
const char *name;
ug_rect_t rect;
// absolute position rect
ug_rect_t rca;
unsigned int flags;
// layouting and elements
// total space used by elements, x and y are the starting coordinates
// for elements
ug_rect_t space;
// origin for in-row and in-column elements
ug_vec2_t c_orig, r_orig;
UG_STACK(ug_element_t) elem_stack;
ug_id_t selected_elem, hover_elem;
} ug_container_t;
// the container flags
enum {
UG_CNT_FLOATING = BIT(0), // is on top of everything else
UG_CNT_RESIZE_RIGHT = BIT(1), // can be resized from the right border
UG_CNT_RESIZE_BOTTOM = BIT(2), // can be resized from the bottom border
UG_CNT_RESIZE_LEFT = BIT(3), // can be resized from the left border
UG_CNT_RESIZE_TOP = BIT(4), // can be resized from the top border
UG_CNT_SCROLL_X = BIT(5), // can have horizontal scrolling
UG_CNT_SCROLL_Y = BIT(6), // can have vertical scrolling
UG_CNT_MOVABLE = BIT(7), // can be moved around
// container state
CNT_STATE_NONE = BIT(30),
CNT_STATE_MOVING = BIT(29),
CNT_STATE_RESIZE_T = BIT(28),
CNT_STATE_RESIZE_B = BIT(27),
CNT_STATE_RESIZE_L = BIT(26),
CNT_STATE_RESIZE_R = BIT(25),
CNT_STATE_RESIZE_D = BIT(24),
CNT_STATE_DELETE = BIT(23), // The container is marked for removal
// layouting
CNT_LAYOUT_COLUMN = BIT(22),
};
// style, defines default height, width, color, margins, borders, etc
// all dimensions should be taken as a reference when doing the layout since
// ultimately it's the layout that decides them. For example when deciding how to
// allocate space one can say that the default size of a region that allocates a
// slider has the style's default dimensions for a slider
typedef struct {
struct { ug_color_t bg, fg; } color;
ug_size_t margin;
struct {
ug_color_t color;
ug_size_t size;
} border;
struct {
struct { ug_color_t bg, fg; } color;
ug_size_t height, font_size;
} title;
struct {
struct { ug_color_t act, bg, fg, sel, br; } color;
ug_size_t font_size, border;
} btn;
} ug_style_t;
// render commands
struct ug_cmd_rect { int x, y, w, h; ug_color_t color; };
struct ug_cmd_text { int x, y, size; ug_color_t color; const char *str; };
typedef struct {
unsigned int type;
union {
struct ug_cmd_rect rect;
struct ug_cmd_text text;
};
} ug_cmd_t;
typedef enum {
UG_CMD_NULL = 0,
UG_CMD_RECT,
UG_CMD_TEXT,
} ug_cmd_type_t;
// window side
enum {
UG_SIDE_TOP = 0,
UG_SIDE_BOTTOM,
UG_SIDE_LEFT,
UG_SIDE_RIGHT,
};
// mouse buttons
enum {
UG_BTN_LEFT = BIT(0),
UG_BTN_MIDDLE = BIT(1),
UG_BTN_RIGHT = BIT(2),
UG_BTN_4 = BIT(3),
UG_BTN_5 = BIT(4),
};
// context
typedef struct {
// some style information
const ug_style_t *style;
// style_px is a style cache where all measurements are already in pixels
const ug_style_t *style_px;
// ppi: pixels per inch
// ppm: pixels per millimeter
// ppd: pixels per dot
float ppi, ppm, ppd;
float last_ppi, last_ppm, last_ppd;
// containers need to know how big the "main container" is so that all
// the relative positioning work
ug_vec2_t size;
ug_rect_t origin;
// which context and element we are hovering
ug_id_t hover_cnt;
// active is updated on mousedown and released on mouseup
// the id of the "active" element, active means different things for
// different elements, for exaple active for a button means to be pressed,
// and for a text box it means to be focused
ug_id_t active_cnt, last_active_cnt;
// id of the selected container, used for layout
// NOTE: since the stacks can be relocated with realloc it is better not
// to use a pointer here, even tough it would be better for efficiency
ug_id_t selected_cnt;
// count the frames for fun
unsigned long int frame;
// mouse data
struct {
ug_vec2_t pos;
ug_vec2_t last_pos;
ug_vec2_t delta;
ug_vec2_t scroll_delta;
// mouse.update: a mask of the mouse buttons that are being updated
unsigned char update;
// mouse.hold: a mask of the buttons that are being held
unsigned char hold;
} mouse;
// keyboard key pressed
struct {
unsigned char update;
unsigned char hold;
} key;
// input text buffer
char input_text[32];
// stacks
UG_STACK(ug_container_t) cnt_stack;
UG_STACK(ug_cmd_t) cmd_stack;
// command stack iterator
int cmd_it;
} ug_ctx_t;
// context initialization
ug_ctx_t * ug_ctx_new(void);
void ug_ctx_free(ug_ctx_t *ctx);
// updates the context with user information
// ppi: pixels per inch, the scale used for all em/mm to pixel calculations
// scale: it is the scale between the physical pixels on the screen and the pixels
// on the buffer, if unsure set to 1.0
int ug_ctx_set_displayinfo(ug_ctx_t *ctx, float scale, float ppi);
int ug_ctx_set_drawableregion(ug_ctx_t *ctx, ug_vec2_t size);
int ug_ctx_set_style(ug_ctx_t *ctx, const ug_style_t *style);
// define containers, name is used as a salt for the container id, all sizes are
// the default ones, resizing is done automagically with internal state
// a floating container can be placed anywhere and can be resized, acts like a
// window inside another window
int ug_container_floating(ug_ctx_t *ctx, const char *name, ug_div_t div);
// like a floating container but cannot be resized
int ug_container_popup(ug_ctx_t *ctx, const char *name, ug_div_t div);
// a menu bar is a container of fixed height, cannot be resized and sits at the
// top of the window
int ug_container_menu_bar(ug_ctx_t *ctx, const char *name, ug_size_t height);
// a sidebar is a variable size container anchored to one side of the window
int ug_container_sidebar(ug_ctx_t *ctx, const char *name, ug_size_t size, int side);
// a body is a container that scales with the window, sits at it's center and cannot
// be resized, it also fills all the available space
int ug_container_body(ug_ctx_t *ctx, const char *name);
// mark a conatiner for removal, it will be freed at the next frame beginning if
// name is NULL then use the selected container
int ug_container_remove(ug_ctx_t *ctx, const char *name);
// get the drawable area of the container, if name is NULL then use the selected
// container
ug_rect_t ug_container_get_rect(ug_ctx_t *ctx, const char *name);
// layouting, the following functions define how different ui elements are placed
// inside the selected container. A new element is aligned respective to the
// previous element and/or to the container, particularly elements can be placed
// in a row or a column.
int ug_layout_row(ug_ctx_t *ctx);
int ug_layout_column(ug_ctx_t *ctx);
int ug_layout_next_row(ug_ctx_t *ctx);
int ug_layout_next_column(ug_ctx_t *ctx);
// elements
int ug_element_button(ug_ctx_t *ctx, const char *name, const char *txt, ug_div_t dim);
int ug_element_textbtn(ug_ctx_t *ctx, const char *name, const char *txt, ug_div_t dim);
// Input functions
int ug_input_mousemove(ug_ctx_t *ctx, int x, int y);
int ug_input_mousedown(ug_ctx_t *ctx, unsigned int mask);
int ug_input_mouseup(ug_ctx_t *ctx, unsigned int mask);
int ug_input_scroll(ug_ctx_t *ctx, int x, int y);
// TODO: other input functions
// Frame handling
int ug_frame_begin(ug_ctx_t *ctx);
int ug_frame_end(ug_ctx_t *ctx);
// Commands
// get the next command, save iteration state inside 'iterator'
ug_cmd_t * ug_cmd_next(ug_ctx_t *ctx);
#undef UG_STACK
#undef BIT
#endif